Completed
Push — master ( af891e...5c98d3 )
by Sam
11:04
created

SSViewerTest::testThemeRetrieval()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 21
nc 2
nop 0
dl 0
loc 28
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
use SilverStripe\ORM\ArrayList;
4
use SilverStripe\ORM\DataObject;
5
use SilverStripe\Security\Member;
6
use SilverStripe\Security\SecurityToken;
7
use SilverStripe\Security\Permission;
8
use SilverStripe\ORM\FieldType\DBField;
9
10
class SSViewerTest extends SapphireTest {
11
12
	/**
13
	 * Backup of $_SERVER global
14
	 *
15
	 * @var array
16
	 */
17
	protected $oldServer = array();
18
19
	protected $extraDataObjects = array(
20
		'SSViewerTest_Object',
21
	);
22
23
	public function setUp() {
0 ignored issues
show
Coding Style introduced by
setUp uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
24
		parent::setUp();
25
		Config::inst()->update('SSViewer', 'source_file_comments', false);
26
		Config::inst()->update('SSViewer_FromString', 'cache_template', false);
27
		AssetStoreTest_SpyStore::activate('SSViewerTest');
28
		$this->oldServer = $_SERVER;
29
	}
30
31
	public function tearDown() {
0 ignored issues
show
Coding Style introduced by
tearDown uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
32
		$_SERVER = $this->oldServer;
33
		AssetStoreTest_SpyStore::reset();
34
		parent::tearDown();
35
	}
36
37
	/**
38
	 * Tests for {@link Config::inst()->get('SSViewer', 'theme')} for different behaviour
39
	 * of user defined themes via {@link SiteConfig} and default theme
40
	 * when no user themes are defined.
41
	 */
42
	public function testCurrentTheme() {
43
		//TODO: SiteConfig moved to CMS
44
		Config::inst()->update('SSViewer', 'theme', 'mytheme');
45
		$this->assertEquals('mytheme', Config::inst()->get('SSViewer', 'theme'),
46
			'Current theme is the default - user has not defined one');
47
	}
48
49
	/**
50
	 * Test that a template without a <head> tag still renders.
51
	 */
52
	public function testTemplateWithoutHeadRenders() {
53
		$data = new ArrayData(array(
54
			'Var' => 'var value'
55
		));
56
57
		$result = $data->renderWith("SSViewerTestPartialTemplate");
58
		$this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U",'',$result)));
59
	}
60
61
	public function testIncludeScopeInheritance() {
62
		$data = $this->getScopeInheritanceTestData();
63
		$expected = array(
64
			'Item 1 - First-ODD top:Item 1',
65
			'Item 2 - EVEN top:Item 2',
66
			'Item 3 - ODD top:Item 3',
67
			'Item 4 - EVEN top:Item 4',
68
			'Item 5 - ODD top:Item 5',
69
			'Item 6 - Last-EVEN top:Item 6',
70
		);
71
72
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritance');
73
		$this->assertExpectedStrings($result, $expected);
74
75
		// reset results for the tests that include arguments (the title is passed as an arg)
76
		$expected = array(
77
			'Item 1 _ Item 1 - First-ODD top:Item 1',
78
			'Item 2 _ Item 2 - EVEN top:Item 2',
79
			'Item 3 _ Item 3 - ODD top:Item 3',
80
			'Item 4 _ Item 4 - EVEN top:Item 4',
81
			'Item 5 _ Item 5 - ODD top:Item 5',
82
			'Item 6 _ Item 6 - Last-EVEN top:Item 6',
83
		);
84
85
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
86
		$this->assertExpectedStrings($result, $expected);
87
	}
88
89
	public function testIncludeTruthyness() {
90
		$data = new ArrayData(array(
91
			'Title' => 'TruthyTest',
92
			'Items' => new ArrayList(array(
93
				new ArrayData(array('Title' => 'Item 1')),
94
				new ArrayData(array('Title' => '')),
95
				new ArrayData(array('Title' => true)),
96
				new ArrayData(array('Title' => false)),
97
				new ArrayData(array('Title' => null)),
98
				new ArrayData(array('Title' => 0)),
99
				new ArrayData(array('Title' => 7))
100
			))
101
		));
102
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
103
104
		// We should not end up with empty values appearing as empty
105
		$expected = array(
106
			'Item 1 _ Item 1 - First-ODD top:Item 1',
107
			'Untitled - EVEN top:',
108
			'1 _ 1 - ODD top:1',
109
			'Untitled - EVEN top:',
110
			'Untitled - ODD top:',
111
			'Untitled - EVEN top:0',
112
			'7 _ 7 - Last-ODD top:7'
113
		);
114
		$this->assertExpectedStrings($result, $expected);
115
	}
116
117
	private function getScopeInheritanceTestData() {
118
		return new ArrayData(array(
119
			'Title' => 'TopTitleValue',
120
			'Items' => new ArrayList(array(
121
				new ArrayData(array('Title' => 'Item 1')),
122
				new ArrayData(array('Title' => 'Item 2')),
123
				new ArrayData(array('Title' => 'Item 3')),
124
				new ArrayData(array('Title' => 'Item 4')),
125
				new ArrayData(array('Title' => 'Item 5')),
126
				new ArrayData(array('Title' => 'Item 6'))
127
			))
128
		));
129
	}
130
131
	private function assertExpectedStrings($result, $expected) {
132
		foreach ($expected as $expectedStr) {
133
			$this->assertTrue(
134
				(boolean) preg_match("/{$expectedStr}/", $result),
135
				"Didn't find '{$expectedStr}' in:\n{$result}"
136
			);
137
		}
138
	}
139
140
	/**
141
	 * Small helper to render templates from strings
142
	 */
143
	public function render($templateString, $data = null, $cacheTemplate = false) {
144
		$t = SSViewer::fromString($templateString, $cacheTemplate);
145
		if(!$data) $data = new SSViewerTestFixture();
146
		return trim(''.$t->process($data));
147
	}
148
149
	public function testRequirements() {
150
		$requirements = $this->getMock("Requirements_Backend", array("javascript", "css"));
151
		$jsFile = FRAMEWORK_DIR . '/tests/forms/a.js';
152
		$cssFile = FRAMEWORK_DIR . '/tests/forms/a.js';
153
154
		$requirements->expects($this->once())->method('javascript')->with($jsFile);
155
		$requirements->expects($this->once())->method('css')->with($cssFile);
156
157
		$origReq = Requirements::backend();
158
		Requirements::set_backend($requirements);
159
		$template = $this->render("<% require javascript($jsFile) %>
160
		<% require css($cssFile) %>");
161
		Requirements::set_backend($origReq);
162
163
		$this->assertFalse((bool)trim($template), "Should be no content in this return.");
164
	}
165
166
	public function testRequirementsCombine(){
167
		$testBackend = Injector::inst()->create('Requirements_Backend');
168
		$testBackend->setSuffixRequirements(false);
169
		//$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js';
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
170
171
		$jsFile = FRAMEWORK_DIR . '/tests/view/themes/javascript/bad.js';
172
		$jsFileContents = file_get_contents(BASE_PATH . '/' . $jsFile);
173
		$testBackend->combineFiles('testRequirementsCombine.js', array($jsFile));
174
175
		// first make sure that our test js file causes an exception to be thrown
176
		try{
177
			require_once('thirdparty/jsmin/jsmin.php');
178
			JSMin::minify($jsFileContents);
179
			$this->fail('JSMin did not throw exception on minify bad file: ');
180
		} catch(Exception $e) {
181
			// exception thrown... good
182
		}
183
184
		// secondly, make sure that requirements is generated, even though minification failed
185
		$testBackend->processCombinedFiles();
186
		$js = array_keys($testBackend->getJavascript());
187
		$combinedTestFilePath = BASE_PATH . reset($js);
188
		$this->assertContains('_combinedfiles/testRequirementsCombine-4c0e97a.js', $combinedTestFilePath);
189
190
		// and make sure the combined content matches the input content, i.e. no loss of functionality
191
		if(!file_exists($combinedTestFilePath)) {
192
			$this->fail('No combined file was created at expected path: '.$combinedTestFilePath);
193
		}
194
		$combinedTestFileContents = file_get_contents($combinedTestFilePath);
195
		$this->assertContains($jsFileContents, $combinedTestFileContents);
196
	}
197
198
199
200
	public function testComments() {
201
		$output = $this->render(<<<SS
202
This is my template<%-- this is a comment --%>This is some content<%-- this is another comment --%>Final content
203
<%-- Alone multi
204
	line comment --%>
205
Some more content
206
Mixing content and <%-- multi
207
	line comment --%> Final final
208
content
209
SS
210
);
211
		$shouldbe = <<<SS
212
This is my templateThis is some contentFinal content
213
214
Some more content
215
Mixing content and  Final final
216
content
217
SS;
218
219
		$this->assertEquals($shouldbe, $output);
220
	}
221
222
	public function testBasicText() {
223
		$this->assertEquals('"', $this->render('"'), 'Double-quotes are left alone');
224
		$this->assertEquals("'", $this->render("'"), 'Single-quotes are left alone');
225
		$this->assertEquals('A', $this->render('\\A'), 'Escaped characters are unescaped');
226
		$this->assertEquals('\\A', $this->render('\\\\A'), 'Escaped back-slashed are correctly unescaped');
227
	}
228
229
	public function testBasicInjection() {
230
		$this->assertEquals('[out:Test]', $this->render('$Test'), 'Basic stand-alone injection');
231
		$this->assertEquals('[out:Test]', $this->render('{$Test}'), 'Basic stand-alone wrapped injection');
232
		$this->assertEquals('A[out:Test]!', $this->render('A$Test!'), 'Basic surrounded injection');
233
		$this->assertEquals('A[out:Test]B', $this->render('A{$Test}B'), 'Basic surrounded wrapped injection');
234
235
		$this->assertEquals('A$B', $this->render('A\\$B'), 'No injection as $ escaped');
236
		$this->assertEquals('A$ B', $this->render('A$ B'), 'No injection as $ not followed by word character');
237
		$this->assertEquals('A{$ B', $this->render('A{$ B'), 'No injection as {$ not followed by word character');
238
239
		$this->assertEquals('{$Test}', $this->render('{\\$Test}'), 'Escapes can be used to avoid injection');
240
		$this->assertEquals('{\\[out:Test]}', $this->render('{\\\\$Test}'),
241
			'Escapes before injections are correctly unescaped');
242
	}
243
244
245
	public function testGlobalVariableCalls() {
246
		$this->assertEquals('automatic', $this->render('$SSViewerTest_GlobalAutomatic'));
247
		$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedByString'));
248
		$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedInArray'));
249
	}
250
251
	public function testGlobalVariableCallsWithArguments() {
252
		$this->assertEquals('zz', $this->render('$SSViewerTest_GlobalThatTakesArguments'));
253
		$this->assertEquals('zFooz', $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo")'));
254
		$this->assertEquals('zFoo:Bar:Bazz',
255
			$this->render('$SSViewerTest_GlobalThatTakesArguments("Foo", "Bar", "Baz")'));
256
		$this->assertEquals('zreferencez',
257
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalReferencedByString)'));
258
	}
259
260
	public function testGlobalVariablesAreEscaped() {
261
		$this->assertEquals('<div></div>', $this->render('$SSViewerTest_GlobalHTMLFragment'));
262
		$this->assertEquals('&lt;div&gt;&lt;/div&gt;', $this->render('$SSViewerTest_GlobalHTMLEscaped'));
263
264
		$this->assertEquals('z<div></div>z',
265
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)'));
266
		$this->assertEquals('z&lt;div&gt;&lt;/div&gt;z',
267
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)'));
268
	}
269
270
	public function testCoreGlobalVariableCalls() {
271
		$this->assertEquals(Director::absoluteBaseURL(),
272
			$this->render('{$absoluteBaseURL}'), 'Director::absoluteBaseURL can be called from within template');
273
		$this->assertEquals(Director::absoluteBaseURL(), $this->render('{$AbsoluteBaseURL}'),
274
			'Upper-case %AbsoluteBaseURL can be called from within template');
275
276
		$this->assertEquals(Director::is_ajax(), $this->render('{$isAjax}'),
277
			'All variations of is_ajax result in the correct call');
278
		$this->assertEquals(Director::is_ajax(), $this->render('{$IsAjax}'),
279
			'All variations of is_ajax result in the correct call');
280
		$this->assertEquals(Director::is_ajax(), $this->render('{$is_ajax}'),
281
			'All variations of is_ajax result in the correct call');
282
		$this->assertEquals(Director::is_ajax(), $this->render('{$Is_ajax}'),
283
			'All variations of is_ajax result in the correct call');
284
285
		$this->assertEquals(i18n::get_locale(), $this->render('{$i18nLocale}'),
286
			'i18n template functions result correct result');
287
		$this->assertEquals(i18n::get_locale(), $this->render('{$get_locale}'),
288
			'i18n template functions result correct result');
289
290
		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentMember}'),
291
			'Member template functions result correct result');
292
		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentUser}'),
293
			'Member template functions result correct result');
294
		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentMember}'),
295
			'Member template functions result correct result');
296
		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentUser}'),
297
			'Member template functions result correct result');
298
299
		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$getSecurityID}'),
300
			'SecurityToken template functions result correct result');
301
		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$SecurityID}'),
302
			'SecurityToken template functions result correct result');
303
304
		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$HasPerm(\'ADMIN\')}'),
305
			'Permissions template functions result correct result');
306
		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$hasPerm(\'ADMIN\')}'),
307
			'Permissions template functions result correct result');
308
	}
309
310
	public function testNonFieldCastingHelpersNotUsedInHasValue() {
311
		// check if Link without $ in front of variable
312
		$result = $this->render(
313
			'A<% if Link %>$Link<% end_if %>B', new SSViewerTest_Object());
314
		$this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if Link %>');
315
316
		// check if Link with $ in front of variable
317
		$result = $this->render(
318
			'A<% if $Link %>$Link<% end_if %>B', new SSViewerTest_Object());
319
		$this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if $Link %>');
320
	}
321
322
	public function testLocalFunctionsTakePriorityOverGlobals() {
323
		$data = new ArrayData(array(
324
			'Page' => new SSViewerTest_Object()
325
		));
326
327
		//call method with lots of arguments
328
		$result = $this->render(
329
			'<% with Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_with %>',$data);
330
		$this->assertEquals("abcdefghijk",$result, "public function can accept up to 11 arguments");
331
332
		//call method that does not exist
333
		$result = $this->render('<% with Page %><% if IDoNotExist %>hello<% end_if %><% end_with %>',$data);
334
		$this->assertEquals("",$result, "Method does not exist - empty result");
335
336
		//call if that does not exist
337
		$result = $this->render('<% with Page %>$IDoNotExist("hello")<% end_with %>',$data);
338
		$this->assertEquals("",$result, "Method does not exist - empty result");
339
340
		//call method with same name as a global method (local call should take priority)
341
		$result = $this->render('<% with Page %>$absoluteBaseURL<% end_with %>',$data);
342
		$this->assertEquals("testLocalFunctionPriorityCalled",$result,
343
			"Local Object's public function called. Did not return the actual baseURL of the current site");
344
	}
345
346
	public function testCurrentScopeLoopWith() {
347
		// Data to run the loop tests on - one sequence of three items, each with a subitem
348
		$data = new ArrayData(array(
349
			'Foo' => new ArrayList(array(
350
				'Subocean' => new ArrayData(array(
351
						'Name' => 'Higher'
352
					)),
353
				new ArrayData(array(
354
					'Sub' => new ArrayData(array(
355
						'Name' => 'SubKid1'
356
					))
357
				)),
358
				new ArrayData(array(
359
					'Sub' => new ArrayData(array(
360
						'Name' => 'SubKid2'
361
					))
362
				)),
363
				new SSViewerTest_Object('Number6')
364
			))
365
		));
366
367
		$result = $this->render(
368
			'<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',$data);
369
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop works");
370
371
		$result = $this->render(
372
			'<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',$data);
373
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop works");
374
375
		$result = $this->render('<% with Foo %>$Count<% end_with %>',$data);
376
		$this->assertEquals("4",$result, "4 items in the DataObjectSet");
377
378
		$result = $this->render('<% with Foo %><% loop Up.Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
379
			. '<% end_if %><% end_loop %><% end_with %>',$data);
380
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop in with Up.Foo scope works");
381
382
		$result = $this->render('<% with Foo %><% loop %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
383
			. '<% end_if %><% end_loop %><% end_with %>',$data);
384
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop in current scope works");
385
	}
386
387
	public function testObjectDotArguments() {
388
		$this->assertEquals(
389
			'[out:TestObject.methodWithOneArgument(one)]
390
				[out:TestObject.methodWithTwoArguments(one,two)]
391
				[out:TestMethod(Arg1,Arg2).Bar.Val]
392
				[out:TestMethod(Arg1,Arg2).Bar]
393
				[out:TestMethod(Arg1,Arg2)]
394
				[out:TestMethod(Arg1).Bar.Val]
395
				[out:TestMethod(Arg1).Bar]
396
				[out:TestMethod(Arg1)]',
397
			$this->render('$TestObject.methodWithOneArgument(one)
398
				$TestObject.methodWithTwoArguments(one,two)
399
				$TestMethod(Arg1, Arg2).Bar.Val
400
				$TestMethod(Arg1, Arg2).Bar
401
				$TestMethod(Arg1, Arg2)
402
				$TestMethod(Arg1).Bar.Val
403
				$TestMethod(Arg1).Bar
404
				$TestMethod(Arg1)')
405
		);
406
	}
407
408
	public function testEscapedArguments() {
409
		$this->assertEquals(
410
			'[out:Foo(Arg1,Arg2).Bar.Val].Suffix
411
				[out:Foo(Arg1,Arg2).Val]_Suffix
412
				[out:Foo(Arg1,Arg2)]/Suffix
413
				[out:Foo(Arg1).Bar.Val]textSuffix
414
				[out:Foo(Arg1).Bar].Suffix
415
				[out:Foo(Arg1)].Suffix
416
				[out:Foo.Bar.Val].Suffix
417
				[out:Foo.Bar].Suffix
418
				[out:Foo].Suffix',
419
			$this->render('{$Foo(Arg1, Arg2).Bar.Val}.Suffix
420
				{$Foo(Arg1, Arg2).Val}_Suffix
421
				{$Foo(Arg1, Arg2)}/Suffix
422
				{$Foo(Arg1).Bar.Val}textSuffix
423
				{$Foo(Arg1).Bar}.Suffix
424
				{$Foo(Arg1)}.Suffix
425
				{$Foo.Bar.Val}.Suffix
426
				{$Foo.Bar}.Suffix
427
				{$Foo}.Suffix')
428
		);
429
	}
430
431
	public function testLoopWhitespace() {
432
		$this->assertEquals(
433
			'before[out:SingleItem.Test]after
434
				beforeTestafter',
435
			$this->render('before<% loop SingleItem %>$Test<% end_loop %>after
436
				before<% loop SingleItem %>Test<% end_loop %>after')
437
		);
438
439
		// The control tags are removed from the output, but no whitespace
440
		// This is a quirk that could be changed, but included in the test to make the current
441
		// behaviour explicit
442
		$this->assertEquals(
443
			'before
444
445
[out:SingleItem.ItemOnItsOwnLine]
446
447
after',
448
			$this->render('before
449
<% loop SingleItem %>
450
$ItemOnItsOwnLine
451
<% end_loop %>
452
after')
453
		);
454
455
		// The whitespace within the control tags is preserve in a loop
456
		// This is a quirk that could be changed, but included in the test to make the current
457
		// behaviour explicit
458
		$this->assertEquals(
459
			'before
460
461
[out:Loop3.ItemOnItsOwnLine]
462
463
[out:Loop3.ItemOnItsOwnLine]
464
465
[out:Loop3.ItemOnItsOwnLine]
466
467
after',
468
			$this->render('before
469
<% loop Loop3 %>
470
$ItemOnItsOwnLine
471
<% end_loop %>
472
after')
473
		);
474
	}
475
476
	public function testControls() {
477
		// Single item controls
478
		$this->assertEquals(
479
			'a[out:Foo.Bar.Item]b
480
				[out:Foo.Bar(Arg1).Item]
481
				[out:Foo(Arg1).Item]
482
				[out:Foo(Arg1,Arg2).Item]
483
				[out:Foo(Arg1,Arg2,Arg3).Item]',
484
			$this->render('<% with Foo.Bar %>a{$Item}b<% end_with %>
485
				<% with Foo.Bar(Arg1) %>$Item<% end_with %>
486
				<% with Foo(Arg1) %>$Item<% end_with %>
487
				<% with Foo(Arg1, Arg2) %>$Item<% end_with %>
488
				<% with Foo(Arg1, Arg2, Arg3) %>$Item<% end_with %>')
489
		);
490
491
		// Loop controls
492
		$this->assertEquals('a[out:Foo.Loop2.Item]ba[out:Foo.Loop2.Item]b',
493
			$this->render('<% loop Foo.Loop2 %>a{$Item}b<% end_loop %>'));
494
495
		$this->assertEquals('[out:Foo.Loop2(Arg1).Item][out:Foo.Loop2(Arg1).Item]',
496
			$this->render('<% loop Foo.Loop2(Arg1) %>$Item<% end_loop %>'));
497
498
		$this->assertEquals('[out:Loop2(Arg1).Item][out:Loop2(Arg1).Item]',
499
			$this->render('<% loop Loop2(Arg1) %>$Item<% end_loop %>'));
500
501
		$this->assertEquals('[out:Loop2(Arg1,Arg2).Item][out:Loop2(Arg1,Arg2).Item]',
502
			$this->render('<% loop Loop2(Arg1, Arg2) %>$Item<% end_loop %>'));
503
504
		$this->assertEquals('[out:Loop2(Arg1,Arg2,Arg3).Item][out:Loop2(Arg1,Arg2,Arg3).Item]',
505
			$this->render('<% loop Loop2(Arg1, Arg2, Arg3) %>$Item<% end_loop %>'));
506
507
	}
508
509
	public function testIfBlocks() {
510
		// Basic test
511
		$this->assertEquals('AC',
512
			$this->render('A<% if NotSet %>B$NotSet<% end_if %>C'));
513
514
		// Nested test
515
		$this->assertEquals('AB1C',
516
			$this->render('A<% if IsSet %>B$NotSet<% if IsSet %>1<% else %>2<% end_if %><% end_if %>C'));
517
518
		// else_if
519
		$this->assertEquals('ACD',
520
			$this->render('A<% if NotSet %>B<% else_if IsSet %>C<% end_if %>D'));
521
		$this->assertEquals('AD',
522
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% end_if %>D'));
523
		$this->assertEquals('ADE',
524
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E'));
525
526
		$this->assertEquals('ADE',
527
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E'));
528
529
		// Dot syntax
530
		$this->assertEquals('ACD',
531
			$this->render('A<% if Foo.NotSet %>B<% else_if Foo.IsSet %>C<% end_if %>D'));
532
		$this->assertEquals('ACD',
533
			$this->render('A<% if Foo.Bar.NotSet %>B<% else_if Foo.Bar.IsSet %>C<% end_if %>D'));
534
535
		// Params
536
		$this->assertEquals('ACD',
537
			$this->render('A<% if NotSet(Param) %>B<% else %>C<% end_if %>D'));
538
		$this->assertEquals('ABD',
539
			$this->render('A<% if IsSet(Param) %>B<% else %>C<% end_if %>D'));
540
541
		// Negation
542
		$this->assertEquals('AC',
543
			$this->render('A<% if not IsSet %>B<% end_if %>C'));
544
		$this->assertEquals('ABC',
545
			$this->render('A<% if not NotSet %>B<% end_if %>C'));
546
547
		// Or
548
		$this->assertEquals('ABD',
549
			$this->render('A<% if IsSet || NotSet %>B<% else_if A %>C<% end_if %>D'));
550
		$this->assertEquals('ACD',
551
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet %>C<% end_if %>D'));
552
		$this->assertEquals('AD',
553
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet3 %>C<% end_if %>D'));
554
		$this->assertEquals('ACD',
555
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet || NotSet %>C<% end_if %>D'));
556
		$this->assertEquals('AD',
557
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet2 || NotSet3 %>C<% end_if %>D'));
558
559
		// Negated Or
560
		$this->assertEquals('ACD',
561
			$this->render('A<% if not IsSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
562
		$this->assertEquals('ABD',
563
			$this->render('A<% if not NotSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
564
		$this->assertEquals('ABD',
565
			$this->render('A<% if NotSet || not AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
566
567
		// And
568
		$this->assertEquals('ABD',
569
			$this->render('A<% if IsSet && AlsoSet %>B<% else_if A %>C<% end_if %>D'));
570
		$this->assertEquals('ACD',
571
			$this->render('A<% if IsSet && NotSet %>B<% else_if IsSet %>C<% end_if %>D'));
572
		$this->assertEquals('AD',
573
			$this->render('A<% if NotSet && NotSet2 %>B<% else_if NotSet3 %>C<% end_if %>D'));
574
		$this->assertEquals('ACD',
575
			$this->render('A<% if IsSet && NotSet %>B<% else_if IsSet && AlsoSet %>C<% end_if %>D'));
576
		$this->assertEquals('AD',
577
			$this->render('A<% if NotSet && NotSet2 %>B<% else_if IsSet && NotSet3 %>C<% end_if %>D'));
578
579
		// Equality
580
		$this->assertEquals('ABC',
581
			$this->render('A<% if RawVal == RawVal %>B<% end_if %>C'));
582
		$this->assertEquals('ACD',
583
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal == RawVal %>C<% end_if %>D'));
584
		$this->assertEquals('ABC',
585
			$this->render('A<% if Right != Wrong %>B<% end_if %>C'));
586
		$this->assertEquals('AD',
587
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D'));
588
589
		// test inequalities with simple numbers
590
		$this->assertEquals('ABD', $this->render('A<% if 5 > 3 %>B<% else %>C<% end_if %>D'));
591
		$this->assertEquals('ABD', $this->render('A<% if 5 >= 3 %>B<% else %>C<% end_if %>D'));
592
		$this->assertEquals('ACD', $this->render('A<% if 3 > 5 %>B<% else %>C<% end_if %>D'));
593
		$this->assertEquals('ACD', $this->render('A<% if 3 >= 5 %>B<% else %>C<% end_if %>D'));
594
595
		$this->assertEquals('ABD', $this->render('A<% if 3 < 5 %>B<% else %>C<% end_if %>D'));
596
		$this->assertEquals('ABD', $this->render('A<% if 3 <= 5 %>B<% else %>C<% end_if %>D'));
597
		$this->assertEquals('ACD', $this->render('A<% if 5 < 3 %>B<% else %>C<% end_if %>D'));
598
		$this->assertEquals('ACD', $this->render('A<% if 5 <= 3 %>B<% else %>C<% end_if %>D'));
599
600
		$this->assertEquals('ABD', $this->render('A<% if 4 <= 4 %>B<% else %>C<% end_if %>D'));
601
		$this->assertEquals('ABD', $this->render('A<% if 4 >= 4 %>B<% else %>C<% end_if %>D'));
602
		$this->assertEquals('ACD', $this->render('A<% if 4 > 4 %>B<% else %>C<% end_if %>D'));
603
		$this->assertEquals('ACD', $this->render('A<% if 4 < 4 %>B<% else %>C<% end_if %>D'));
604
605
		// empty else_if and else tags, if this would not be supported,
606
		// the output would stop after A, thereby failing the assert
607
		$this->assertEquals('AD', $this->render('A<% if IsSet %><% else %><% end_if %>D'));
608
		$this->assertEquals('AD',
609
			$this->render('A<% if NotSet %><% else_if IsSet %><% else %><% end_if %>D'));
610
		$this->assertEquals('AD',
611
			$this->render('A<% if NotSet %><% else_if AlsoNotSet %><% else %><% end_if %>D'));
612
613
		// Bare words with ending space
614
		$this->assertEquals('ABC',
615
			$this->render('A<% if "RawVal" == RawVal %>B<% end_if %>C'));
616
617
		// Else
618
		$this->assertEquals('ADE',
619
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E'));
620
621
		// Empty if with else
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
622
		$this->assertEquals('ABC',
623
			$this->render('A<% if NotSet %><% else %>B<% end_if %>C'));
624
	}
625
626
	public function testBaseTagGeneration() {
627
		// XHTML wil have a closed base tag
628
		$tmpl1 = '<?xml version="1.0" encoding="UTF-8"?>
629
			<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
630
				. ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
631
			<html>
632
				<head><% base_tag %></head>
633
				<body><p>test</p><body>
634
			</html>';
635
		$this->assertRegExp('/<head><base href=".*" \/><\/head>/', $this->render($tmpl1));
636
637
		// HTML4 and 5 will only have it for IE
638
		$tmpl2 = '<!DOCTYPE html>
639
			<html>
640
				<head><% base_tag %></head>
641
				<body><p>test</p><body>
642
			</html>';
643
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
644
			$this->render($tmpl2));
645
646
647
		$tmpl3 = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
648
			<html>
649
				<head><% base_tag %></head>
650
				<body><p>test</p><body>
651
			</html>';
652
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
653
			$this->render($tmpl3));
654
655
		// Check that the content negotiator converts to the equally legal formats
656
		$negotiator = new ContentNegotiator();
657
658
		$response = new SS_HTTPResponse($this->render($tmpl1));
659
		$negotiator->html($response);
660
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
661
			$response->getBody());
662
663
		$response = new SS_HTTPResponse($this->render($tmpl1));
664
		$negotiator->xhtml($response);
665
		$this->assertRegExp('/<head><base href=".*" \/><\/head>/', $response->getBody());
666
	}
667
668
	public function testIncludeWithArguments() {
669
		$this->assertEquals(
670
			$this->render('<% include SSViewerTestIncludeWithArguments %>'),
671
			'<p>[out:Arg1]</p><p>[out:Arg2]</p>'
672
		);
673
674
		$this->assertEquals(
675
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A %>'),
676
			'<p>A</p><p>[out:Arg2]</p>'
677
		);
678
679
		$this->assertEquals(
680
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A, Arg2=B %>'),
681
			'<p>A</p><p>B</p>'
682
		);
683
684
		$this->assertEquals(
685
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'),
686
			'<p>A Bare String</p><p>B Bare String</p>'
687
		);
688
689
		$this->assertEquals(
690
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=$B %>',
691
				new ArrayData(array('B' => 'Bar'))),
692
			'<p>A</p><p>Bar</p>'
693
		);
694
695
		$this->assertEquals(
696
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A" %>',
697
				new ArrayData(array('Arg1' => 'Foo', 'Arg2' => 'Bar'))),
698
			'<p>A</p><p>Bar</p>'
699
		);
700
701
		$this->assertEquals(
702
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>',
703
				new ArrayData(array('Items' => new ArrayList(array(
704
					new ArrayData(array('Title' => 'Foo')),
705
					new ArrayData(array('Title' => 'Bar'))
706
				))))),
707
			'SomeArg - Foo - Bar - SomeArg'
708
		);
709
710
		$this->assertEquals(
711
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInWith Title="A" %>',
712
				new ArrayData(array('Item' => new ArrayData(array('Title' =>'B'))))),
713
			'A - B - A'
714
		);
715
716
		$this->assertEquals(
717
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>',
718
				new ArrayData(array(
719
					'Item' => new ArrayData(array(
720
						'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
721
					)))
722
				)),
723
			'A - B - C - B - A'
724
		);
725
726
		$this->assertEquals(
727
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithUpAndTop Title="A" %>',
728
				new ArrayData(array(
729
					'Item' => new ArrayData(array(
730
						'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
731
					)))
732
				)),
733
			'A - A - A'
734
		);
735
736
		$data = new ArrayData(array(
737
			'Nested' => new ArrayData(array(
738
				'Object' => new ArrayData(array('Key' => 'A'))
739
			)),
740
			'Object' => new ArrayData(array('Key' => 'B'))
741
		));
742
743
		$tmpl = SSViewer::fromString('<% include SSViewerTestIncludeObjectArguments A=$Nested.Object, B=$Object %>');
744
		$res  = $tmpl->process($data);
745
		$this->assertEqualIgnoringWhitespace('A B', $res, 'Objects can be passed as named arguments');
0 ignored issues
show
Unused Code introduced by
The call to SSViewerTest::assertEqualIgnoringWhitespace() has too many arguments starting with 'Objects can be passed as named arguments'.

This check compares calls to functions or methods with their respective definitions. If the call has more 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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
746
	}
747
748
	public function testNamespaceInclude() {
749
		$data = new ArrayData([]);
750
751
		$this->assertEquals(
752
			"tests:( NamespaceInclude\n )",
753
			$this->render('tests:( <% include Namespace\NamespaceInclude %> )', $data),
754
			'Backslashes work for namespace references in includes'
755
		);
756
757
		$this->assertEquals(
758
			"tests:( NamespaceInclude\n )",
759
			$this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data),
760
			'Forward slashes work for namespace references in includes'
761
		);
762
	}
763
764
765
	public function testRecursiveInclude() {
766
		$view = new SSViewer(array('Includes/SSViewerTestRecursiveInclude'));
767
768
		$data = new ArrayData(array(
769
			'Title' => 'A',
770
			'Children' => new ArrayList(array(
771
				new ArrayData(array(
772
					'Title' => 'A1',
773
					'Children' => new ArrayList(array(
774
						new ArrayData(array( 'Title' => 'A1 i', )),
775
						new ArrayData(array( 'Title' => 'A1 ii', )),
776
					)),
777
				)),
778
				new ArrayData(array( 'Title' => 'A2', )),
779
				new ArrayData(array( 'Title' => 'A3', )),
780
			)),
781
		));
782
783
		$result = $view->process($data);
784
		// We don't care about whitespace
785
		$rationalisedResult = trim(preg_replace('/\s+/', ' ', $result));
786
787
		$this->assertEquals('A A1 A1 i A1 ii A2 A3', $rationalisedResult);
788
	}
789
790
	public function assertEqualIgnoringWhitespace($a, $b) {
791
		$this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b));
792
	}
793
794
	/**
795
	 * See {@link ViewableDataTest} for more extensive casting tests,
796
	 * this test just ensures that basic casting is correctly applied during template parsing.
797
	 */
798
	public function testCastingHelpers() {
799
		$vd = new SSViewerTest_ViewableData();
800
		$vd->TextValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property TextValue does not exist on object<SSViewerTest_ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
801
		$vd->HTMLValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property HTMLValue does not exist on object<SSViewerTest_ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
802
		$vd->UncastedValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property UncastedValue does not exist on object<SSViewerTest_ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
803
804
		// Value casted as "Text"
805
		$this->assertEquals(
806
			'&lt;b&gt;html&lt;/b&gt;',
807
			$t = SSViewer::fromString('$TextValue')->process($vd)
808
		);
809
		$this->assertEquals(
810
			'<b>html</b>',
811
			$t = SSViewer::fromString('$TextValue.RAW')->process($vd)
812
		);
813
		$this->assertEquals(
814
			'&lt;b&gt;html&lt;/b&gt;',
815
			$t = SSViewer::fromString('$TextValue.XML')->process($vd)
816
		);
817
818
		// Value casted as "HTMLText"
819
		$this->assertEquals(
820
			'<b>html</b>',
821
			$t = SSViewer::fromString('$HTMLValue')->process($vd)
822
		);
823
		$this->assertEquals(
824
			'<b>html</b>',
825
			$t = SSViewer::fromString('$HTMLValue.RAW')->process($vd)
826
		);
827
		$this->assertEquals(
828
			'&lt;b&gt;html&lt;/b&gt;',
829
			$t = SSViewer::fromString('$HTMLValue.XML')->process($vd)
830
		);
831
832
		// Uncasted value (falls back to ViewableData::$default_cast="Text")
833
		$vd = new SSViewerTest_ViewableData();
834
		$vd->UncastedValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property UncastedValue does not exist on object<SSViewerTest_ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
835
		$this->assertEquals(
836
			'&lt;b&gt;html&lt;/b&gt;',
837
			$t = SSViewer::fromString('$UncastedValue')->process($vd)
838
		);
839
		$this->assertEquals(
840
			'<b>html</b>',
841
			$t = SSViewer::fromString('$UncastedValue.RAW')->process($vd)
842
		);
843
		$this->assertEquals(
844
			'&lt;b&gt;html&lt;/b&gt;',
845
			$t = SSViewer::fromString('$UncastedValue.XML')->process($vd)
846
		);
847
	}
848
849
	public function testSSViewerBasicIteratorSupport() {
850
		$data = new ArrayData(array(
851
			'Set' => new ArrayList(array(
852
				new SSViewerTest_Object("1"),
853
				new SSViewerTest_Object("2"),
854
				new SSViewerTest_Object("3"),
855
				new SSViewerTest_Object("4"),
856
				new SSViewerTest_Object("5"),
857
				new SSViewerTest_Object("6"),
858
				new SSViewerTest_Object("7"),
859
				new SSViewerTest_Object("8"),
860
				new SSViewerTest_Object("9"),
861
				new SSViewerTest_Object("10"),
862
			))
863
		));
864
865
		//base test
866
		$result = $this->render('<% loop Set %>$Number<% end_loop %>',$data);
867
		$this->assertEquals("12345678910",$result,"Numbers rendered in order");
868
869
		//test First
870
		$result = $this->render('<% loop Set %><% if First %>$Number<% end_if %><% end_loop %>',$data);
871
		$this->assertEquals("1",$result,"Only the first number is rendered");
872
873
		//test Last
874
		$result = $this->render('<% loop Set %><% if Last %>$Number<% end_if %><% end_loop %>',$data);
875
		$this->assertEquals("10",$result,"Only the last number is rendered");
876
877
		//test Even
878
		$result = $this->render('<% loop Set %><% if Even() %>$Number<% end_if %><% end_loop %>',$data);
879
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
880
881
		//test Even with quotes
882
		$result = $this->render('<% loop Set %><% if Even("1") %>$Number<% end_if %><% end_loop %>',$data);
883
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
884
885
		//test Even without quotes
886
		$result = $this->render('<% loop Set %><% if Even(1) %>$Number<% end_if %><% end_loop %>',$data);
887
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
888
889
		//test Even with zero-based start index
890
		$result = $this->render('<% loop Set %><% if Even("0") %>$Number<% end_if %><% end_loop %>',$data);
891
		$this->assertEquals("13579",$result,"Even (with zero-based index) numbers rendered in order");
892
893
		//test Odd
894
		$result = $this->render('<% loop Set %><% if Odd %>$Number<% end_if %><% end_loop %>',$data);
895
		$this->assertEquals("13579",$result,"Odd numbers rendered in order");
896
897
		//test FirstLast
898
		$result = $this->render('<% loop Set %><% if FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>',$data);
899
		$this->assertEquals("1first10last",$result,"First and last numbers rendered in order");
900
901
		//test Middle
902
		$result = $this->render('<% loop Set %><% if Middle %>$Number<% end_if %><% end_loop %>',$data);
903
		$this->assertEquals("23456789",$result,"Middle numbers rendered in order");
904
905
		//test MiddleString
906
		$result = $this->render('<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %>'
907
			. '<% end_loop %>',$data);
908
		$this->assertEquals("2middle3middle4middle5middle6middle7middle8middle9middle",$result,
909
			"Middle numbers rendered in order");
910
911
		//test EvenOdd
912
		$result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>',$data);
913
		$this->assertEquals("oddevenoddevenoddevenoddevenoddeven",$result,
914
			"Even and Odd is returned in sequence numbers rendered in order");
915
916
		//test Pos
917
		$result = $this->render('<% loop Set %>$Pos<% end_loop %>',$data);
918
		$this->assertEquals("12345678910", $result, '$Pos is rendered in order');
919
920
		//test Pos
921
		$result = $this->render('<% loop Set %>$Pos(0)<% end_loop %>',$data);
922
		$this->assertEquals("0123456789", $result, '$Pos(0) is rendered in order');
923
924
		//test FromEnd
925
		$result = $this->render('<% loop Set %>$FromEnd<% end_loop %>',$data);
926
		$this->assertEquals("10987654321", $result, '$FromEnd is rendered in order');
927
928
		//test FromEnd
929
		$result = $this->render('<% loop Set %>$FromEnd(0)<% end_loop %>',$data);
930
		$this->assertEquals("9876543210", $result, '$FromEnd(0) rendered in order');
931
932
		//test Total
933
		$result = $this->render('<% loop Set %>$TotalItems<% end_loop %>',$data);
934
		$this->assertEquals("10101010101010101010",$result,"10 total items X 10 are returned");
935
936
		//test Modulus
937
		$result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>',$data);
938
		$this->assertEquals("1010101010",$result,"1-indexed pos modular divided by 2 rendered in order");
939
940
		//test MultipleOf 3
941
		$result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>',$data);
942
		$this->assertEquals("369",$result,"Only numbers that are multiples of 3 are returned");
943
944
		//test MultipleOf 4
945
		$result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>',$data);
946
		$this->assertEquals("48",$result,"Only numbers that are multiples of 4 are returned");
947
948
		//test MultipleOf 5
949
		$result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>',$data);
950
		$this->assertEquals("510",$result,"Only numbers that are multiples of 5 are returned");
951
952
		//test MultipleOf 10
953
		$result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>',$data);
954
		$this->assertEquals("10",$result,"Only numbers that are multiples of 10 (with 1-based indexing) are returned");
955
956
		//test MultipleOf 9 zero-based
957
		$result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>',$data);
958
		$this->assertEquals("110",$result,
959
			"Only numbers that are multiples of 9 with zero-based indexing are returned. (The first and last item)");
960
961
		//test MultipleOf 11
962
		$result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>',$data);
963
		$this->assertEquals("",$result,"Only numbers that are multiples of 11 are returned. I.e. nothing returned");
964
	}
965
966
	/**
967
	 * Test $Up works when the scope $Up refers to was entered with a "with" block
968
	 */
969
	public function testUpInWith() {
970
971
		// Data to run the loop tests on - three levels deep
972
		$data = new ArrayData(array(
973
			'Name' => 'Top',
974
			'Foo' => new ArrayData(array(
975
				'Name' => 'Foo',
976
				'Bar' => new ArrayData(array(
977
					'Name' => 'Bar',
978
					'Baz' => new ArrayData(array(
979
						'Name' => 'Baz'
980
					)),
981
					'Qux' => new ArrayData(array(
982
						'Name' => 'Qux'
983
					))
984
				))
985
			))
986
		));
987
988
		// Basic functionality
989
		$this->assertEquals('BarFoo',
990
			$this->render('<% with Foo %><% with Bar %>{$Name}{$Up.Name}<% end_with %><% end_with %>', $data));
991
992
		// Two level with block, up refers to internally referenced Bar
993
		$this->assertEquals('BarFoo',
994
			$this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data));
995
996
		// Stepping up & back down the scope tree
997
		$this->assertEquals('BazBarQux',
998
			$this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Name}{$Up.Qux.Name}<% end_with %>', $data));
999
1000
		// Using $Up in a with block
1001
		$this->assertEquals('BazBarQux',
1002
			$this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Qux.Name}<% end_with %>'
1003
				.'<% end_with %>', $data));
1004
1005
		// Stepping up & back down the scope tree with with blocks
1006
		$this->assertEquals('BazBarQuxBarBaz',
1007
			$this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Qux %>{$Name}<% end_with %>'
1008
				. '{$Name}<% end_with %>{$Name}<% end_with %>', $data));
1009
1010
		// Using $Up.Up, where first $Up points to a previous scope entered using $Up, thereby skipping up to Foo
1011
		$this->assertEquals('Foo',
1012
			$this->render('<% with Foo.Bar.Baz %><% with Up %><% with Qux %>{$Up.Up.Name}<% end_with %><% end_with %>'
1013
				. '<% end_with %>', $data));
1014
1015
		// Using $Up.Up, where first $Up points to an Up used in a local scope lookup, should still skip to Foo
1016
		$this->assertEquals('Foo',
1017
			$this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Up.Name}<% end_with %>', $data));
1018
	}
1019
1020
	/**
1021
	 * Test $Up works when the scope $Up refers to was entered with a "loop" block
1022
	 */
1023
	public function testUpInLoop(){
1024
1025
		// Data to run the loop tests on - one sequence of three items, each with a subitem
1026
		$data = new ArrayData(array(
1027
			'Name' => 'Top',
1028
			'Foo' => new ArrayList(array(
1029
				new ArrayData(array(
1030
					'Name' => '1',
1031
					'Sub' => new ArrayData(array(
1032
						'Name' => 'Bar'
1033
					))
1034
				)),
1035
				new ArrayData(array(
1036
					'Name' => '2',
1037
					'Sub' => new ArrayData(array(
1038
						'Name' => 'Baz'
1039
					))
1040
				)),
1041
				new ArrayData(array(
1042
					'Name' => '3',
1043
					'Sub' => new ArrayData(array(
1044
						'Name' => 'Qux'
1045
					))
1046
				))
1047
			))
1048
		));
1049
1050
		// Make sure inside a loop, $Up refers to the current item of the loop
1051
		$this->assertEqualIgnoringWhitespace(
1052
			'111 222 333',
1053
			$this->render(
1054
				'<% loop $Foo %>$Name<% with $Sub %>$Up.Name<% end_with %>$Name<% end_loop %>',
1055
				$data
1056
			)
1057
		);
1058
1059
		// Make sure inside a loop, looping over $Up uses a separate iterator,
1060
		// and doesn't interfere with the original iterator
1061
		$this->assertEqualIgnoringWhitespace(
1062
			'1Bar123Bar1 2Baz123Baz2 3Qux123Qux3',
1063
			$this->render(
1064
				'<% loop $Foo %>
1065
					$Name
1066
					<% with $Sub %>
1067
						$Name
1068
						<% loop $Up %>$Name<% end_loop %>
1069
						$Name
1070
					<% end_with %>
1071
					$Name
1072
				<% end_loop %>',
1073
				$data
1074
			)
1075
		);
1076
1077
		// Make sure inside a loop, looping over $Up uses a separate iterator,
1078
		// and doesn't interfere with the original iterator or local lookups
1079
		$this->assertEqualIgnoringWhitespace(
1080
			'1 Bar1 123 1Bar 1   2 Baz2 123 2Baz 2   3 Qux3 123 3Qux 3',
1081
			$this->render(
1082
				'<% loop $Foo %>
1083
					$Name
1084
					<% with $Sub %>
1085
						{$Name}{$Up.Name}
1086
						<% loop $Up %>$Name<% end_loop %>
1087
						{$Up.Name}{$Name}
1088
					<% end_with %>
1089
					$Name
1090
				<% end_loop %>',
1091
				$data
1092
			)
1093
		);
1094
	}
1095
1096
	/**
1097
	 * Test that nested loops restore the loop variables correctly when pushing and popping states
1098
	 */
1099
	public function testNestedLoops(){
1100
1101
		// Data to run the loop tests on - one sequence of three items, one with child elements
1102
		// (of a different size to the main sequence)
1103
		$data = new ArrayData(array(
1104
			'Foo' => new ArrayList(array(
1105
				new ArrayData(array(
1106
					'Name' => '1',
1107
					'Children' => new ArrayList(array(
1108
						new ArrayData(array(
1109
							'Name' => 'a'
1110
						)),
1111
						new ArrayData(array(
1112
							'Name' => 'b'
1113
						)),
1114
					)),
1115
				)),
1116
				new ArrayData(array(
1117
					'Name' => '2',
1118
					'Children' => new ArrayList(),
1119
				)),
1120
				new ArrayData(array(
1121
					'Name' => '3',
1122
					'Children' => new ArrayList(),
1123
				)),
1124
			)),
1125
		));
1126
1127
		// Make sure that including a loop inside a loop will not destroy the internal count of
1128
		// items, checked by using "Last"
1129
		$this->assertEqualIgnoringWhitespace(
1130
			'1ab23last',
1131
			$this->render('<% loop $Foo %>$Name<% loop Children %>$Name<% end_loop %><% if Last %>last<% end_if %>'
1132
				. '<% end_loop %>', $data
1133
			)
1134
		);
1135
	}
1136
1137
	public function testLayout() {
1138
		$self = $this;
1139
1140
		$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
1141
			$template = new SSViewer(array('Page'));
1142
			$self->assertEquals("Foo\n\n", $template->process(new ArrayData(array())));
1143
1144
			$template = new SSViewer(array('Shortcodes', 'Page'));
1145
			$self->assertEquals("[file_link]\n\n", $template->process(new ArrayData(array())));
1146
		});
1147
	}
1148
1149
	/**
1150
	 * @covers SSViewer::get_templates_by_class()
1151
	 */
1152
	public function testGetTemplatesByClass() {
1153
		$self = $this;
1154
		$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
1155
			// Test passing a string
1156
			$templates = SSViewer::get_templates_by_class(
1157
				'TestNamespace\SSViewerTestModel_Controller',
1158
				'',
1159
				'Controller'
1160
			);
1161
			$self->assertEquals([
1162
				'TestNamespace\SSViewerTestModel_Controller',
1163
    			'Controller',
1164
			], $templates);
1165
1166
			// Test to ensure we're stopping at the base class.
1167
			$templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTestModel_Controller', '', 'TestNamespace\SSViewerTestModel_Controller');
1168
			$self->assertCount(1, $templates);
1169
1170
			// Make sure we can filter our templates by suffix.
1171
			$templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTestModel', '_Controller');
1172
			$self->assertCount(1, $templates);
1173
1174
			// Let's throw something random in there.
1175
			$self->setExpectedException('InvalidArgumentException');
1176
			$templates = SSViewer::get_templates_by_class(array());
1177
		});
1178
	}
1179
1180
	public function testRewriteHashlinks() {
0 ignored issues
show
Coding Style introduced by
testRewriteHashlinks uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1181
		$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1182
		Config::inst()->update('SSViewer', 'rewrite_hash_links', true);
1183
1184
		$_SERVER['HTTP_HOST'] = 'www.mysite.com';
1185
		$_SERVER['REQUEST_URI'] = '//file.com?foo"onclick="alert(\'xss\')""';
1186
1187
		// Emulate SSViewer::process()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1188
		// Note that leading double slashes have been rewritten to prevent these being mis-interepreted
1189
		// as protocol-less absolute urls
1190
		$base = Convert::raw2att('/file.com?foo"onclick="alert(\'xss\')""');
1191
1192
		$tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinks_' . sha1(rand()) . '.ss';
1193
1194
		// Note: SSViewer_FromString doesn't rewrite hash links.
1195
		file_put_contents($tmplFile, '<!DOCTYPE html>
1196
			<html>
1197
				<head><% base_tag %></head>
1198
				<body>
1199
				<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>
1200
				$ExternalInsertedLink
1201
				<a class="inline" href="#anchor">InlineLink</a>
1202
				$InsertedLink
1203
				<svg><use xlink:href="#sprite"></use></svg>
1204
				<body>
1205
			</html>');
1206
		$tmpl = new SSViewer($tmplFile);
1207
		$obj = new ViewableData();
1208
		$obj->InsertedLink = DBField::create_field(
0 ignored issues
show
Documentation introduced by
The property InsertedLink does not exist on object<ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1209
			'HTMLFragment',
1210
			'<a class="inserted" href="#anchor">InsertedLink</a>'
1211
		);
1212
		$obj->ExternalInsertedLink = DBField::create_field(
0 ignored issues
show
Documentation introduced by
The property ExternalInsertedLink does not exist on object<ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1213
			'HTMLFragment',
1214
			'<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>'
1215
		);
1216
		$result = $tmpl->process($obj);
1217
		$this->assertContains(
1218
			'<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
1219
			$result
1220
		);
1221
		$this->assertContains(
1222
			'<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>',
1223
			$result
1224
		);
1225
		$this->assertContains(
1226
			'<a class="inline" href="' . $base . '#anchor">InlineLink</a>',
1227
			$result
1228
		);
1229
		$this->assertContains(
1230
			'<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>',
1231
			$result
1232
		);
1233
		$this->assertContains(
1234
			'<svg><use xlink:href="#sprite"></use></svg>',
1235
			$result,
1236
			'SSTemplateParser should only rewrite anchor hrefs'
1237
		);
1238
1239
		unlink($tmplFile);
1240
1241
		Config::inst()->update('SSViewer', 'rewrite_hash_links', $orig);
1242
	}
1243
1244
	public function testRewriteHashlinksInPhpMode() {
1245
		$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1246
		Config::inst()->update('SSViewer', 'rewrite_hash_links', 'php');
1247
1248
		$tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinksInPhpMode_' . sha1(rand()) . '.ss';
1249
1250
		// Note: SSViewer_FromString doesn't rewrite hash links.
1251
		file_put_contents($tmplFile, '<!DOCTYPE html>
1252
			<html>
1253
				<head><% base_tag %></head>
1254
				<body>
1255
				<a class="inline" href="#anchor">InlineLink</a>
1256
				$InsertedLink
1257
				<svg><use xlink:href="#sprite"></use></svg>
1258
				<body>
1259
			</html>');
1260
		$tmpl = new SSViewer($tmplFile);
1261
		$obj = new ViewableData();
1262
		$obj->InsertedLink = DBField::create_field(
0 ignored issues
show
Documentation introduced by
The property InsertedLink does not exist on object<ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1263
			'HTMLFragment',
1264
			'<a class="inserted" href="#anchor">InsertedLink</a>'
1265
		);
1266
		$result = $tmpl->process($obj);
1267
1268
		$code = <<<'EOC'
1269
<a class="inserted" href="<?php echo Convert::raw2att(preg_replace("/^(\/)+/", "/", $_SERVER['REQUEST_URI'])); ?>#anchor">InsertedLink</a>
1270
EOC;
1271
		$this->assertContains($code, $result);
1272
		// TODO Fix inline links in PHP mode
1273
		// $this->assertContains(
1274
		// 	'<a class="inline" href="<?php echo str_replace(',
1275
		// 	$result
1276
		// );
1277
		$this->assertContains(
1278
			'<svg><use xlink:href="#sprite"></use></svg>',
1279
			$result,
1280
			'SSTemplateParser should only rewrite anchor hrefs'
1281
		);
1282
1283
		unlink($tmplFile);
1284
1285
		Config::inst()->update('SSViewer', 'rewrite_hash_links', $orig);
1286
	}
1287
1288
	public function testRenderWithSourceFileComments() {
1289
		$origEnv = Config::inst()->get('Director', 'environment_type');
1290
		Config::inst()->update('Director', 'environment_type', 'dev');
1291
		Config::inst()->update('SSViewer', 'source_file_comments', true);
1292
		$i = FRAMEWORK_PATH . '/tests/templates/Includes';
1293
		$f = FRAMEWORK_PATH . '/tests/templates/SSViewerTestComments';
1294
		$templates = array(
1295
			array(
1296
				'name' => 'SSViewerTestCommentsFullSource',
1297
				'expected' => ""
1298
					. "<!doctype html>"
1299
					. "<!-- template $f/SSViewerTestCommentsFullSource.ss -->"
1300
					. "<html>"
1301
					. "\t<head></head>"
1302
					. "\t<body></body>"
1303
					. "</html>"
1304
					. "<!-- end template $f/SSViewerTestCommentsFullSource.ss -->",
1305
			),
1306
			array(
1307
				'name' => 'SSViewerTestCommentsFullSourceHTML4Doctype',
1308
				'expected' => ""
1309
					. "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML "
1310
					. "4.01//EN\"\t\t\"http://www.w3.org/TR/html4/strict.dtd\">"
1311
					. "<!-- template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->"
1312
					. "<html>"
1313
					. "\t<head></head>"
1314
					. "\t<body></body>"
1315
					. "</html>"
1316
					. "<!-- end template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->",
1317
			),
1318
			array(
1319
				'name' => 'SSViewerTestCommentsFullSourceNoDoctype',
1320
				'expected' => ""
1321
					. "<html><!-- template $f/SSViewerTestCommentsFullSourceNoDoctype.ss -->"
1322
					. "\t<head></head>"
1323
					. "\t<body></body>"
1324
					. "<!-- end template $f/SSViewerTestCommentsFullSourceNoDoctype.ss --></html>",
1325
			),
1326
			array(
1327
				'name' => 'SSViewerTestCommentsFullSourceIfIE',
1328
				'expected' => ""
1329
					. "<!doctype html>"
1330
					. "<!-- template $f/SSViewerTestCommentsFullSourceIfIE.ss -->"
1331
					. "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1332
					. "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1333
					. "<!--[if !IE]><!--> <html class='no-ie'> <!--<![endif]-->"
1334
					. "\t<head></head>"
1335
					. "\t<body></body>"
1336
					. "</html>"
1337
					. "<!-- end template $f/SSViewerTestCommentsFullSourceIfIE.ss -->",
1338
			),
1339
			array(
1340
				'name' => 'SSViewerTestCommentsFullSourceIfIENoDoctype',
1341
				'expected' => ""
1342
					. "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1343
					. "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1344
					. "<!--[if !IE]><!--> <html class='no-ie'>"
1345
					. "<!-- template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss -->"
1346
					. " <!--<![endif]-->"
1347
					. "\t<head></head>"
1348
					. "\t<body></body>"
1349
					. "<!-- end template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss --></html>",
1350
			),
1351
			array(
1352
				'name' => 'SSViewerTestCommentsPartialSource',
1353
				'expected' =>
1354
				"<!-- template $f/SSViewerTestCommentsPartialSource.ss -->"
1355
					. "<div class='typography'></div>"
1356
					. "<!-- end template $f/SSViewerTestCommentsPartialSource.ss -->",
1357
			),
1358
			array(
1359
				'name' => 'SSViewerTestCommentsWithInclude',
1360
				'expected' =>
1361
				"<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
1362
					. "<div class='typography'>"
1363
					. "<!-- include 'SSViewerTestCommentsInclude' -->"
1364
					. "<!-- template $i/SSViewerTestCommentsInclude.ss -->"
1365
					. "Included"
1366
					. "<!-- end template $i/SSViewerTestCommentsInclude.ss -->"
1367
					. "<!-- end include 'SSViewerTestCommentsInclude' -->"
1368
					. "</div>"
1369
					. "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
1370
			),
1371
		);
1372
		foreach ($templates as $template) {
1373
			$this->_renderWithSourceFileComments('SSViewerTestComments/'.$template['name'], $template['expected']);
1374
		}
1375
		Config::inst()->update('SSViewer', 'source_file_comments', false);
1376
		Config::inst()->update('Director', 'environment_type', $origEnv);
1377
	}
1378
	private function _renderWithSourceFileComments($name, $expected) {
1379
		$viewer = new SSViewer(array($name));
1380
		$data = new ArrayData(array());
1381
		$result = $viewer->process($data);
1382
		$expected = str_replace(array("\r", "\n"), '', $expected);
1383
		$result = str_replace(array("\r", "\n"), '', $result);
1384
		$this->assertEquals($result, $expected);
1385
	}
1386
1387
	public function testLoopIteratorIterator() {
1388
		$list = new PaginatedList(new ArrayList());
1389
		$viewer = new SSViewer_FromString('<% loop List %>$ID - $FirstName<br /><% end_loop %>');
1390
		$result = $viewer->process(new ArrayData(array('List' => $list)));
1391
		$this->assertEquals($result, '');
1392
	}
1393
1394
	public function testProcessOnlyIncludesRequirementsOnce() {
1395
		$template = new SSViewer(array('SSViewerTestProcess'));
1396
		$basePath = dirname($this->getCurrentRelativePath()) . '/forms';
1397
1398
		$backend = Injector::inst()->create('Requirements_Backend');
1399
		$backend->setCombinedFilesEnabled(false);
1400
		$backend->combineFiles(
1401
			'RequirementsTest_ab.css',
1402
			array(
1403
				$basePath . '/RequirementsTest_a.css',
1404
				$basePath . '/RequirementsTest_b.css'
1405
			)
1406
		);
1407
1408
		Requirements::set_backend($backend);
1409
1410
		$this->assertEquals(1, substr_count($template->process(array()), "a.css"));
1411
		$this->assertEquals(1, substr_count($template->process(array()), "b.css"));
1412
1413
		// if we disable the requirements then we should get nothing
1414
		$template->includeRequirements(false);
1415
		$this->assertEquals(0, substr_count($template->process(array()), "a.css"));
1416
		$this->assertEquals(0, substr_count($template->process(array()), "b.css"));
1417
	}
1418
1419
	public function testRequireCallInTemplateInclude() {
1420
		//TODO undo skip test on the event that templates ever obtain the ability to reference MODULE_DIR (or something to that effect)
1421
		if(FRAMEWORK_DIR === 'framework') {
1422
			$template = new SSViewer(array('SSViewerTestProcess'));
1423
1424
			Requirements::set_suffix_requirements(false);
1425
1426
			$this->assertEquals(1, substr_count(
1427
				$template->process(array()),
1428
				"tests/forms/RequirementsTest_a.js"
1429
			));
1430
		}
1431
		else {
1432
			$this->markTestSkipped('Requirement will always fail if the framework dir is not '.
1433
				'named \'framework\', since templates require hard coded paths');
1434
		}
1435
	}
1436
1437
	public function testCallsWithArguments() {
1438
		$data = new ArrayData(array(
1439
			'Set' => new ArrayList(array(
1440
				new SSViewerTest_Object("1"),
1441
				new SSViewerTest_Object("2"),
1442
				new SSViewerTest_Object("3"),
1443
				new SSViewerTest_Object("4"),
1444
				new SSViewerTest_Object("5"),
1445
			)),
1446
			'Level' => new SSViewerTest_LevelTest(1),
1447
			'Nest' => array(
1448
				'Level' => new SSViewerTest_LevelTest(2),
1449
			),
1450
		));
1451
1452
		$tests = array(
1453
			'$Level.output(1)' => '1-1',
1454
			'$Nest.Level.output($Set.First.Number)' => '2-1',
1455
			'<% with $Set %>$Up.Level.output($First.Number)<% end_with %>' => '1-1',
1456
			'<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>' => '2-1',
1457
			'<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>' => '2-12-22-32-42-5',
1458
			'<% loop $Set %>$Top.Level.output($Number)<% end_loop %>' => '1-11-21-31-41-5',
1459
			'<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>' => '2-1',
1460
			'<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>' => '1-5',
1461
			'<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>' => '5-hi',
1462
			'<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>' => '!0',
1463
			'<% with $Nest %>
1464
				<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
1465
			<% end_with %>' => '1-hi',
1466
			'<% with $Nest %>
1467
				<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
1468
			<% end_with %>' => '!0!1!2!3!4',
1469
		);
1470
1471
		foreach($tests as $template => $expected) {
1472
			$this->assertEquals($expected, trim($this->render($template, $data)));
1473
		}
1474
	}
1475
1476
	public function testClosedBlockExtension() {
1477
		$count = 0;
1478
		$parser = new SSTemplateParser();
1479
		$parser->addClosedBlock(
1480
			'test',
1481
			function (&$res) use (&$count) {
0 ignored issues
show
Unused Code introduced by
The parameter $res is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1482
				$count++;
1483
			}
1484
		);
1485
1486
		$template = new SSViewer_FromString("<% test %><% end_test %>", $parser);
1487
		$template->process(new SSViewerTestFixture());
1488
1489
		$this->assertEquals(1, $count);
1490
	}
1491
1492
	public function testOpenBlockExtension() {
1493
		$count = 0;
1494
		$parser = new SSTemplateParser();
1495
		$parser->addOpenBlock(
1496
			'test',
1497
			function (&$res) use (&$count) {
0 ignored issues
show
Unused Code introduced by
The parameter $res is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1498
				$count++;
1499
			}
1500
		);
1501
1502
		$template = new SSViewer_FromString("<% test %>", $parser);
1503
		$template->process(new SSViewerTestFixture());
1504
1505
		$this->assertEquals(1, $count);
1506
	}
1507
1508
	/**
1509
	 * Tests if caching for SSViewer_FromString is working
1510
	 */
1511
	public function testFromStringCaching() {
1512
		$content = 'Test content';
1513
		$cacheFile = TEMP_FOLDER . '/.cache.' . sha1($content);
1514
		if (file_exists($cacheFile)) {
1515
			unlink($cacheFile);
1516
		}
1517
1518
		// Test global behaviors
1519
		$this->render($content, null, null);
1520
		$this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
1521
1522
		Config::inst()->update('SSViewer_FromString', 'cache_template', true);
1523
		$this->render($content, null, null);
1524
		$this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
1525
		unlink($cacheFile);
1526
1527
		// Test instance behaviors
1528
		$this->render($content, null, false);
1529
		$this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
1530
1531
		$this->render($content, null, true);
1532
		$this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
1533
		unlink($cacheFile);
1534
	}
1535
}
1536
1537
/**
1538
 * A test fixture that will echo back the template item
1539
 */
1540
class SSViewerTestFixture extends ViewableData {
1541
	protected $name;
1542
1543
	public function __construct($name = null) {
1544
		$this->name = $name;
1545
		parent::__construct();
1546
	}
1547
1548
1549
	private function argedName($fieldName, $arguments) {
1550
		$childName = $this->name ? "$this->name.$fieldName" : $fieldName;
1551
		if($arguments) return $childName . '(' . implode(',', $arguments) . ')';
1552
		else return $childName;
1553
	}
1554
	public function obj($fieldName, $arguments=null, $cache=false, $cacheName=null) {
1555
		$childName = $this->argedName($fieldName, $arguments);
1556
1557
		// Special field name Loop### to create a list
1558
		if(preg_match('/^Loop([0-9]+)$/', $fieldName, $matches)) {
1559
			$output = new ArrayList();
1560
			for($i=0;$i<$matches[1];$i++) $output->push(new SSViewerTestFixture($childName));
1561
			return $output;
1562
1563
		} else if(preg_match('/NotSet/i', $fieldName)) {
1564
			return new ViewableData();
1565
1566
		} else {
1567
			return new SSViewerTestFixture($childName);
1568
		}
1569
	}
1570
1571
1572
	public function XML_val($fieldName, $arguments = null, $cache = false) {
1573
		if(preg_match('/NotSet/i', $fieldName)) {
1574
			return '';
1575
		} else if(preg_match('/Raw/i', $fieldName)) {
1576
			return $fieldName;
1577
		} else {
1578
			return '[out:' . $this->argedName($fieldName, $arguments) . ']';
1579
		}
1580
	}
1581
1582
	public function hasValue($fieldName, $arguments = null, $cache = true) {
1583
		return (bool)$this->XML_val($fieldName, $arguments);
1584
	}
1585
}
1586
1587
class SSViewerTest_ViewableData extends ViewableData implements TestOnly {
1588
1589
	private static $default_cast = 'Text';
1590
1591
	private static $casting = array(
1592
		'TextValue' => 'Text',
1593
		'HTMLValue' => 'HTMLFragment'
1594
	);
1595
1596
	public function methodWithOneArgument($arg1) {
1597
		return "arg1:{$arg1}";
1598
	}
1599
1600
	public function methodWithTwoArguments($arg1, $arg2) {
1601
		return "arg1:{$arg1},arg2:{$arg2}";
1602
	}
1603
}
1604
1605
class SSViewerTest_Object extends DataObject implements TestOnly {
1606
1607
	public $number = null;
1608
1609
	private static $casting = array(
1610
		'Link' => 'Text',
1611
	);
1612
1613
1614
	public function __construct($number = null) {
1615
		parent::__construct();
1616
		$this->number = $number;
1617
	}
1618
1619
	public function Number() {
1620
		return $this->number;
1621
	}
1622
1623
	public function absoluteBaseURL() {
1624
		return "testLocalFunctionPriorityCalled";
1625
	}
1626
1627
	public function lotsOfArguments11($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k) {
1628
		return $a. $b. $c. $d. $e. $f. $g. $h. $i. $j. $k;
1629
	}
1630
1631
	public function Link() {
1632
		return 'some/url.html';
1633
	}
1634
}
1635
1636
class SSViewerTest_GlobalProvider implements TemplateGlobalProvider, TestOnly {
1637
1638
	public static function get_template_global_variables() {
1639
		return array(
1640
			'SSViewerTest_GlobalHTMLFragment' => array('method' => 'get_html', 'casting' => 'HTMLFragment'),
1641
			'SSViewerTest_GlobalHTMLEscaped' => array('method' => 'get_html'),
1642
1643
			'SSViewerTest_GlobalAutomatic',
1644
			'SSViewerTest_GlobalReferencedByString' => 'get_reference',
1645
			'SSViewerTest_GlobalReferencedInArray' => array('method' => 'get_reference'),
1646
1647
			'SSViewerTest_GlobalThatTakesArguments' => array('method' => 'get_argmix', 'casting' => 'HTMLFragment')
1648
1649
		);
1650
	}
1651
1652
	public static function get_html() {
1653
		return '<div></div>';
1654
	}
1655
1656
	public static function SSViewerTest_GlobalAutomatic() {
1657
		return 'automatic';
1658
	}
1659
1660
	public static function get_reference() {
1661
		return 'reference';
1662
	}
1663
1664
	public static function get_argmix() {
1665
		$args = func_get_args();
1666
		return 'z' . implode(':', $args) . 'z';
1667
	}
1668
1669
}
1670
1671
class SSViewerTest_LevelTest extends ViewableData implements TestOnly {
1672
	protected $depth;
1673
1674
	public function __construct($depth = 1) {
1675
		$this->depth = $depth;
1676
	}
1677
1678
	public function output($val) {
1679
		return "$this->depth-$val";
1680
	}
1681
1682
	public function forLoop($number) {
1683
		$ret = array();
1684
		for($i = 0; $i < (int)$number; ++$i) {
1685
			$ret[] = new SSViewerTest_Object("!$i");
1686
		}
1687
		return new ArrayList($ret);
1688
	}
1689
1690
	public function forWith($number) {
1691
		return new self($number);
1692
	}
1693
}
1694