Completed
Push — namespace-template ( 367a36...f9db4f )
by Sam
07:20
created

SSViewerTest::testLoopIteratorIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 6
rs 9.4285
cc 1
eloc 5
nc 1
nop 0
1
<?php
2
3
class SSViewerTest extends SapphireTest {
4
5
	/**
6
	 * Backup of $_SERVER global
7
	 *
8
	 * @var array
9
	 */
10
	protected $oldServer = array();
11
12
	protected $extraDataObjects = array(
13
		'SSViewerTest_Object',
14
	);
15
16
	public function setUp() {
17
		parent::setUp();
18
		Config::inst()->update('SSViewer', 'source_file_comments', false);
19
		Config::inst()->update('SSViewer_FromString', 'cache_template', false);
20
		AssetStoreTest_SpyStore::activate('SSViewerTest');
21
		$this->oldServer = $_SERVER;
22
	}
23
24
	public function tearDown() {
25
		$_SERVER = $this->oldServer;
26
		AssetStoreTest_SpyStore::reset();
27
		parent::tearDown();
28
	}
29
30
	/**
31
	 * Tests for {@link Config::inst()->get('SSViewer', 'theme')} for different behaviour
32
	 * of user defined themes via {@link SiteConfig} and default theme
33
	 * when no user themes are defined.
34
	 */
35
	public function testCurrentTheme() {
36
		//TODO: SiteConfig moved to CMS
37
		Config::inst()->update('SSViewer', 'theme', 'mytheme');
38
		$this->assertEquals('mytheme', Config::inst()->get('SSViewer', 'theme'),
39
			'Current theme is the default - user has not defined one');
40
	}
41
42
	/**
43
	 * Test that a template without a <head> tag still renders.
44
	 */
45
	public function testTemplateWithoutHeadRenders() {
46
		$data = new ArrayData(array(
47
			'Var' => 'var value'
48
		));
49
50
		$result = $data->renderWith("SSViewerTestPartialTemplate");
51
		$this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U",'',$result)));
52
	}
53
54
	public function testIncludeScopeInheritance() {
55
		$data = $this->getScopeInheritanceTestData();
56
		$expected = array(
57
			'Item 1 - First-ODD top:Item 1',
58
			'Item 2 - EVEN top:Item 2',
59
			'Item 3 - ODD top:Item 3',
60
			'Item 4 - EVEN top:Item 4',
61
			'Item 5 - ODD top:Item 5',
62
			'Item 6 - Last-EVEN top:Item 6',
63
		);
64
65
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritance');
66
		$this->assertExpectedStrings($result, $expected);
67
68
		// reset results for the tests that include arguments (the title is passed as an arg)
69
		$expected = array(
70
			'Item 1 _ Item 1 - First-ODD top:Item 1',
71
			'Item 2 _ Item 2 - EVEN top:Item 2',
72
			'Item 3 _ Item 3 - ODD top:Item 3',
73
			'Item 4 _ Item 4 - EVEN top:Item 4',
74
			'Item 5 _ Item 5 - ODD top:Item 5',
75
			'Item 6 _ Item 6 - Last-EVEN top:Item 6',
76
		);
77
78
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
79
		$this->assertExpectedStrings($result, $expected);
80
	}
81
82
	public function testIncludeTruthyness() {
83
		$data = new ArrayData(array(
84
			'Title' => 'TruthyTest',
85
			'Items' => new ArrayList(array(
86
				new ArrayData(array('Title' => 'Item 1')),
87
				new ArrayData(array('Title' => '')),
88
				new ArrayData(array('Title' => true)),
89
				new ArrayData(array('Title' => false)),
90
				new ArrayData(array('Title' => null)),
91
				new ArrayData(array('Title' => 0)),
92
				new ArrayData(array('Title' => 7))
93
			))
94
		));
95
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
96
97
		// We should not end up with empty values appearing as empty
98
		$expected = array(
99
			'Item 1 _ Item 1 - First-ODD top:Item 1',
100
			'Untitled - EVEN top:',
101
			'1 _ 1 - ODD top:1',
102
			'Untitled - EVEN top:',
103
			'Untitled - ODD top:',
104
			'Untitled - EVEN top:0',
105
			'7 _ 7 - Last-ODD top:7'
106
		);
107
		$this->assertExpectedStrings($result, $expected);
108
	}
109
110
	private function getScopeInheritanceTestData() {
111
		return new ArrayData(array(
112
			'Title' => 'TopTitleValue',
113
			'Items' => new ArrayList(array(
114
				new ArrayData(array('Title' => 'Item 1')),
115
				new ArrayData(array('Title' => 'Item 2')),
116
				new ArrayData(array('Title' => 'Item 3')),
117
				new ArrayData(array('Title' => 'Item 4')),
118
				new ArrayData(array('Title' => 'Item 5')),
119
				new ArrayData(array('Title' => 'Item 6'))
120
			))
121
		));
122
	}
123
124
	private function assertExpectedStrings($result, $expected) {
125
		foreach ($expected as $expectedStr) {
126
			$this->assertTrue(
127
				(boolean) preg_match("/{$expectedStr}/", $result),
128
				"Didn't find '{$expectedStr}' in:\n{$result}"
129
			);
130
		}
131
	}
132
133
	/**
134
	 * Small helper to render templates from strings
135
	 */
136
	public function render($templateString, $data = null, $cacheTemplate = false) {
137
		$t = SSViewer::fromString($templateString, $cacheTemplate);
138
		if(!$data) $data = new SSViewerTestFixture();
139
		return trim(''.$t->process($data));
140
	}
141
142
	public function testRequirements() {
143
		$requirements = $this->getMock("Requirements_Backend", array("javascript", "css"));
144
		$jsFile = FRAMEWORK_DIR . '/tests/forms/a.js';
145
		$cssFile = FRAMEWORK_DIR . '/tests/forms/a.js';
146
147
		$requirements->expects($this->once())->method('javascript')->with($jsFile);
148
		$requirements->expects($this->once())->method('css')->with($cssFile);
149
150
		$origReq = Requirements::backend();
151
		Requirements::set_backend($requirements);
152
		$template = $this->render("<% require javascript($jsFile) %>
153
		<% require css($cssFile) %>");
154
		Requirements::set_backend($origReq);
155
156
		$this->assertFalse((bool)trim($template), "Should be no content in this return.");
157
	}
158
159
	public function testRequirementsCombine(){
160
		$testBackend = Injector::inst()->create('Requirements_Backend');
161
		$testBackend->setSuffixRequirements(false);
162
		//$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js';
163
164
		$jsFile = FRAMEWORK_DIR . '/tests/view/themes/javascript/bad.js';
165
		$jsFileContents = file_get_contents(BASE_PATH . '/' . $jsFile);
166
		$testBackend->combineFiles('testRequirementsCombine.js', array($jsFile));
167
168
		// first make sure that our test js file causes an exception to be thrown
169
		try{
170
			require_once('thirdparty/jsmin/jsmin.php');
171
			$content = JSMin::minify($content);
0 ignored issues
show
Bug introduced by
The variable $content seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Unused Code introduced by
$content is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
172
			$this->fail('JSMin did not throw exception on minify bad file: ');
173
		} catch(Exception $e) {
174
			// exception thrown... good
175
		}
176
177
		// secondly, make sure that requirements is generated, even though minification failed
178
		$testBackend->processCombinedFiles();
179
		$js = $testBackend->getJavascript();
180
		$combinedTestFilePath = BASE_PATH . reset($js);
181
		$this->assertContains('_combinedfiles/testRequirementsCombine-4c0e97a.js', $combinedTestFilePath);
182
183
		// and make sure the combined content matches the input content, i.e. no loss of functionality
184
		if(!file_exists($combinedTestFilePath)) {
185
			$this->fail('No combined file was created at expected path: '.$combinedTestFilePath);
186
		}
187
		$combinedTestFileContents = file_get_contents($combinedTestFilePath);
188
		$this->assertContains($jsFileContents, $combinedTestFileContents);
189
	}
190
191
192
193
	public function testComments() {
194
		$output = $this->render(<<<SS
195
This is my template<%-- this is a comment --%>This is some content<%-- this is another comment --%>Final content
196
<%-- Alone multi
197
	line comment --%>
198
Some more content
199
Mixing content and <%-- multi
200
	line comment --%> Final final
201
content
202
SS
203
);
204
		$shouldbe = <<<SS
205
This is my templateThis is some contentFinal content
206
207
Some more content
208
Mixing content and  Final final
209
content
210
SS;
211
212
		$this->assertEquals($shouldbe, $output);
213
	}
214
215
	public function testBasicText() {
216
		$this->assertEquals('"', $this->render('"'), 'Double-quotes are left alone');
217
		$this->assertEquals("'", $this->render("'"), 'Single-quotes are left alone');
218
		$this->assertEquals('A', $this->render('\\A'), 'Escaped characters are unescaped');
219
		$this->assertEquals('\\A', $this->render('\\\\A'), 'Escaped back-slashed are correctly unescaped');
220
	}
221
222
	public function testBasicInjection() {
223
		$this->assertEquals('[out:Test]', $this->render('$Test'), 'Basic stand-alone injection');
224
		$this->assertEquals('[out:Test]', $this->render('{$Test}'), 'Basic stand-alone wrapped injection');
225
		$this->assertEquals('A[out:Test]!', $this->render('A$Test!'), 'Basic surrounded injection');
226
		$this->assertEquals('A[out:Test]B', $this->render('A{$Test}B'), 'Basic surrounded wrapped injection');
227
228
		$this->assertEquals('A$B', $this->render('A\\$B'), 'No injection as $ escaped');
229
		$this->assertEquals('A$ B', $this->render('A$ B'), 'No injection as $ not followed by word character');
230
		$this->assertEquals('A{$ B', $this->render('A{$ B'), 'No injection as {$ not followed by word character');
231
232
		$this->assertEquals('{$Test}', $this->render('{\\$Test}'), 'Escapes can be used to avoid injection');
233
		$this->assertEquals('{\\[out:Test]}', $this->render('{\\\\$Test}'),
234
			'Escapes before injections are correctly unescaped');
235
	}
236
237
238
	public function testGlobalVariableCalls() {
239
		$this->assertEquals('automatic', $this->render('$SSViewerTest_GlobalAutomatic'));
240
		$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedByString'));
241
		$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedInArray'));
242
	}
243
244
	public function testGlobalVariableCallsWithArguments() {
245
		$this->assertEquals('zz', $this->render('$SSViewerTest_GlobalThatTakesArguments'));
246
		$this->assertEquals('zFooz', $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo")'));
247
		$this->assertEquals('zFoo:Bar:Bazz',
248
			$this->render('$SSViewerTest_GlobalThatTakesArguments("Foo", "Bar", "Baz")'));
249
		$this->assertEquals('zreferencez',
250
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalReferencedByString)'));
251
	}
252
253
	public function testGlobalVariablesAreEscaped() {
254
		$this->assertEquals('<div></div>', $this->render('$SSViewerTest_GlobalHTMLFragment'));
255
		$this->assertEquals('&lt;div&gt;&lt;/div&gt;', $this->render('$SSViewerTest_GlobalHTMLEscaped'));
256
257
		$this->assertEquals('z<div></div>z',
258
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)'));
259
		$this->assertEquals('z&lt;div&gt;&lt;/div&gt;z',
260
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)'));
261
	}
262
263
	public function testCoreGlobalVariableCalls() {
264
		$this->assertEquals(Director::absoluteBaseURL(),
265
			$this->render('{$absoluteBaseURL}'), 'Director::absoluteBaseURL can be called from within template');
266
		$this->assertEquals(Director::absoluteBaseURL(), $this->render('{$AbsoluteBaseURL}'),
267
			'Upper-case %AbsoluteBaseURL can be called from within template');
268
269
		$this->assertEquals(Director::is_ajax(), $this->render('{$isAjax}'),
270
			'All variations of is_ajax result in the correct call');
271
		$this->assertEquals(Director::is_ajax(), $this->render('{$IsAjax}'),
272
			'All variations of is_ajax result in the correct call');
273
		$this->assertEquals(Director::is_ajax(), $this->render('{$is_ajax}'),
274
			'All variations of is_ajax result in the correct call');
275
		$this->assertEquals(Director::is_ajax(), $this->render('{$Is_ajax}'),
276
			'All variations of is_ajax result in the correct call');
277
278
		$this->assertEquals(i18n::get_locale(), $this->render('{$i18nLocale}'),
279
			'i18n template functions result correct result');
280
		$this->assertEquals(i18n::get_locale(), $this->render('{$get_locale}'),
281
			'i18n template functions result correct result');
282
283
		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentMember}'),
284
			'Member template functions result correct result');
285
		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentUser}'),
286
			'Member template functions result correct result');
287
		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentMember}'),
288
			'Member template functions result correct result');
289
		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentUser}'),
290
			'Member template functions result correct result');
291
292
		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$getSecurityID}'),
293
			'SecurityToken template functions result correct result');
294
		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$SecurityID}'),
295
			'SecurityToken template functions result correct result');
296
297
		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$HasPerm(\'ADMIN\')}'),
298
			'Permissions template functions result correct result');
299
		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$hasPerm(\'ADMIN\')}'),
300
			'Permissions template functions result correct result');
301
	}
302
303
	public function testNonFieldCastingHelpersNotUsedInHasValue() {
304
		// check if Link without $ in front of variable
305
		$result = $this->render(
306
			'A<% if Link %>$Link<% end_if %>B', new SSViewerTest_Object());
307
		$this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if Link %>');
308
309
		// check if Link with $ in front of variable
310
		$result = $this->render(
311
			'A<% if $Link %>$Link<% end_if %>B', new SSViewerTest_Object());
312
		$this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if $Link %>');
313
	}
314
315
	public function testLocalFunctionsTakePriorityOverGlobals() {
316
		$data = new ArrayData(array(
317
			'Page' => new SSViewerTest_Object()
318
		));
319
320
		//call method with lots of arguments
321
		$result = $this->render(
322
			'<% with Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_with %>',$data);
323
		$this->assertEquals("abcdefghijk",$result, "public function can accept up to 11 arguments");
324
325
		//call method that does not exist
326
		$result = $this->render('<% with Page %><% if IDoNotExist %>hello<% end_if %><% end_with %>',$data);
327
		$this->assertEquals("",$result, "Method does not exist - empty result");
328
329
		//call if that does not exist
330
		$result = $this->render('<% with Page %>$IDoNotExist("hello")<% end_with %>',$data);
331
		$this->assertEquals("",$result, "Method does not exist - empty result");
332
333
		//call method with same name as a global method (local call should take priority)
334
		$result = $this->render('<% with Page %>$absoluteBaseURL<% end_with %>',$data);
335
		$this->assertEquals("testLocalFunctionPriorityCalled",$result,
336
			"Local Object's public function called. Did not return the actual baseURL of the current site");
337
	}
338
339
	public function testCurrentScopeLoopWith() {
340
		// Data to run the loop tests on - one sequence of three items, each with a subitem
341
		$data = new ArrayData(array(
342
			'Foo' => new ArrayList(array(
343
				'Subocean' => new ArrayData(array(
344
						'Name' => 'Higher'
345
					)),
346
				new ArrayData(array(
347
					'Sub' => new ArrayData(array(
348
						'Name' => 'SubKid1'
349
					))
350
				)),
351
				new ArrayData(array(
352
					'Sub' => new ArrayData(array(
353
						'Name' => 'SubKid2'
354
					))
355
				)),
356
				new SSViewerTest_Object('Number6')
357
			))
358
		));
359
360
		$result = $this->render(
361
			'<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',$data);
362
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop works");
363
364
		$result = $this->render(
365
			'<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',$data);
366
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop works");
367
368
		$result = $this->render('<% with Foo %>$Count<% end_with %>',$data);
369
		$this->assertEquals("4",$result, "4 items in the DataObjectSet");
370
371
		$result = $this->render('<% with Foo %><% loop Up.Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
372
			. '<% end_if %><% end_loop %><% end_with %>',$data);
373
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop in with Up.Foo scope works");
374
375
		$result = $this->render('<% with Foo %><% loop %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
376
			. '<% end_if %><% end_loop %><% end_with %>',$data);
377
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop in current scope works");
378
	}
379
380
	public function testObjectDotArguments() {
381
		$this->assertEquals(
382
			'[out:TestObject.methodWithOneArgument(one)]
383
				[out:TestObject.methodWithTwoArguments(one,two)]
384
				[out:TestMethod(Arg1,Arg2).Bar.Val]
385
				[out:TestMethod(Arg1,Arg2).Bar]
386
				[out:TestMethod(Arg1,Arg2)]
387
				[out:TestMethod(Arg1).Bar.Val]
388
				[out:TestMethod(Arg1).Bar]
389
				[out:TestMethod(Arg1)]',
390
			$this->render('$TestObject.methodWithOneArgument(one)
391
				$TestObject.methodWithTwoArguments(one,two)
392
				$TestMethod(Arg1, Arg2).Bar.Val
393
				$TestMethod(Arg1, Arg2).Bar
394
				$TestMethod(Arg1, Arg2)
395
				$TestMethod(Arg1).Bar.Val
396
				$TestMethod(Arg1).Bar
397
				$TestMethod(Arg1)')
398
		);
399
	}
400
401
	public function testEscapedArguments() {
402
		$this->assertEquals(
403
			'[out:Foo(Arg1,Arg2).Bar.Val].Suffix
404
				[out:Foo(Arg1,Arg2).Val]_Suffix
405
				[out:Foo(Arg1,Arg2)]/Suffix
406
				[out:Foo(Arg1).Bar.Val]textSuffix
407
				[out:Foo(Arg1).Bar].Suffix
408
				[out:Foo(Arg1)].Suffix
409
				[out:Foo.Bar.Val].Suffix
410
				[out:Foo.Bar].Suffix
411
				[out:Foo].Suffix',
412
			$this->render('{$Foo(Arg1, Arg2).Bar.Val}.Suffix
413
				{$Foo(Arg1, Arg2).Val}_Suffix
414
				{$Foo(Arg1, Arg2)}/Suffix
415
				{$Foo(Arg1).Bar.Val}textSuffix
416
				{$Foo(Arg1).Bar}.Suffix
417
				{$Foo(Arg1)}.Suffix
418
				{$Foo.Bar.Val}.Suffix
419
				{$Foo.Bar}.Suffix
420
				{$Foo}.Suffix')
421
		);
422
	}
423
424
	public function testLoopWhitespace() {
425
		$this->assertEquals(
426
			'before[out:SingleItem.Test]after
427
				beforeTestafter',
428
			$this->render('before<% loop SingleItem %>$Test<% end_loop %>after
429
				before<% loop SingleItem %>Test<% end_loop %>after')
430
		);
431
432
		// The control tags are removed from the output, but no whitespace
433
		// This is a quirk that could be changed, but included in the test to make the current
434
		// behaviour explicit
435
		$this->assertEquals(
436
			'before
437
438
[out:SingleItem.ItemOnItsOwnLine]
439
440
after',
441
			$this->render('before
442
<% loop SingleItem %>
443
$ItemOnItsOwnLine
444
<% end_loop %>
445
after')
446
		);
447
448
		// The whitespace within the control tags is preserve in a loop
449
		// This is a quirk that could be changed, but included in the test to make the current
450
		// behaviour explicit
451
		$this->assertEquals(
452
			'before
453
454
[out:Loop3.ItemOnItsOwnLine]
455
456
[out:Loop3.ItemOnItsOwnLine]
457
458
[out:Loop3.ItemOnItsOwnLine]
459
460
after',
461
			$this->render('before
462
<% loop Loop3 %>
463
$ItemOnItsOwnLine
464
<% end_loop %>
465
after')
466
		);
467
	}
468
469
	public function testControls() {
470
		// Single item controls
471
		$this->assertEquals(
472
			'a[out:Foo.Bar.Item]b
473
				[out:Foo.Bar(Arg1).Item]
474
				[out:Foo(Arg1).Item]
475
				[out:Foo(Arg1,Arg2).Item]
476
				[out:Foo(Arg1,Arg2,Arg3).Item]',
477
			$this->render('<% with Foo.Bar %>a{$Item}b<% end_with %>
478
				<% with Foo.Bar(Arg1) %>$Item<% end_with %>
479
				<% with Foo(Arg1) %>$Item<% end_with %>
480
				<% with Foo(Arg1, Arg2) %>$Item<% end_with %>
481
				<% with Foo(Arg1, Arg2, Arg3) %>$Item<% end_with %>')
482
		);
483
484
		// Loop controls
485
		$this->assertEquals('a[out:Foo.Loop2.Item]ba[out:Foo.Loop2.Item]b',
486
			$this->render('<% loop Foo.Loop2 %>a{$Item}b<% end_loop %>'));
487
488
		$this->assertEquals('[out:Foo.Loop2(Arg1).Item][out:Foo.Loop2(Arg1).Item]',
489
			$this->render('<% loop Foo.Loop2(Arg1) %>$Item<% end_loop %>'));
490
491
		$this->assertEquals('[out:Loop2(Arg1).Item][out:Loop2(Arg1).Item]',
492
			$this->render('<% loop Loop2(Arg1) %>$Item<% end_loop %>'));
493
494
		$this->assertEquals('[out:Loop2(Arg1,Arg2).Item][out:Loop2(Arg1,Arg2).Item]',
495
			$this->render('<% loop Loop2(Arg1, Arg2) %>$Item<% end_loop %>'));
496
497
		$this->assertEquals('[out:Loop2(Arg1,Arg2,Arg3).Item][out:Loop2(Arg1,Arg2,Arg3).Item]',
498
			$this->render('<% loop Loop2(Arg1, Arg2, Arg3) %>$Item<% end_loop %>'));
499
500
	}
501
502
	public function testIfBlocks() {
503
		// Basic test
504
		$this->assertEquals('AC',
505
			$this->render('A<% if NotSet %>B$NotSet<% end_if %>C'));
506
507
		// Nested test
508
		$this->assertEquals('AB1C',
509
			$this->render('A<% if IsSet %>B$NotSet<% if IsSet %>1<% else %>2<% end_if %><% end_if %>C'));
510
511
		// else_if
512
		$this->assertEquals('ACD',
513
			$this->render('A<% if NotSet %>B<% else_if IsSet %>C<% end_if %>D'));
514
		$this->assertEquals('AD',
515
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% end_if %>D'));
516
		$this->assertEquals('ADE',
517
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E'));
518
519
		$this->assertEquals('ADE',
520
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E'));
521
522
		// Dot syntax
523
		$this->assertEquals('ACD',
524
			$this->render('A<% if Foo.NotSet %>B<% else_if Foo.IsSet %>C<% end_if %>D'));
525
		$this->assertEquals('ACD',
526
			$this->render('A<% if Foo.Bar.NotSet %>B<% else_if Foo.Bar.IsSet %>C<% end_if %>D'));
527
528
		// Params
529
		$this->assertEquals('ACD',
530
			$this->render('A<% if NotSet(Param) %>B<% else %>C<% end_if %>D'));
531
		$this->assertEquals('ABD',
532
			$this->render('A<% if IsSet(Param) %>B<% else %>C<% end_if %>D'));
533
534
		// Negation
535
		$this->assertEquals('AC',
536
			$this->render('A<% if not IsSet %>B<% end_if %>C'));
537
		$this->assertEquals('ABC',
538
			$this->render('A<% if not NotSet %>B<% end_if %>C'));
539
540
		// Or
541
		$this->assertEquals('ABD',
542
			$this->render('A<% if IsSet || NotSet %>B<% else_if A %>C<% end_if %>D'));
543
		$this->assertEquals('ACD',
544
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet %>C<% end_if %>D'));
545
		$this->assertEquals('AD',
546
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet3 %>C<% end_if %>D'));
547
		$this->assertEquals('ACD',
548
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet || NotSet %>C<% end_if %>D'));
549
		$this->assertEquals('AD',
550
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet2 || NotSet3 %>C<% end_if %>D'));
551
552
		// Negated Or
553
		$this->assertEquals('ACD',
554
			$this->render('A<% if not IsSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
555
		$this->assertEquals('ABD',
556
			$this->render('A<% if not NotSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
557
		$this->assertEquals('ABD',
558
			$this->render('A<% if NotSet || not AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
559
560
		// And
561
		$this->assertEquals('ABD',
562
			$this->render('A<% if IsSet && AlsoSet %>B<% else_if A %>C<% end_if %>D'));
563
		$this->assertEquals('ACD',
564
			$this->render('A<% if IsSet && NotSet %>B<% else_if IsSet %>C<% end_if %>D'));
565
		$this->assertEquals('AD',
566
			$this->render('A<% if NotSet && NotSet2 %>B<% else_if NotSet3 %>C<% end_if %>D'));
567
		$this->assertEquals('ACD',
568
			$this->render('A<% if IsSet && NotSet %>B<% else_if IsSet && AlsoSet %>C<% end_if %>D'));
569
		$this->assertEquals('AD',
570
			$this->render('A<% if NotSet && NotSet2 %>B<% else_if IsSet && NotSet3 %>C<% end_if %>D'));
571
572
		// Equality
573
		$this->assertEquals('ABC',
574
			$this->render('A<% if RawVal == RawVal %>B<% end_if %>C'));
575
		$this->assertEquals('ACD',
576
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal == RawVal %>C<% end_if %>D'));
577
		$this->assertEquals('ABC',
578
			$this->render('A<% if Right != Wrong %>B<% end_if %>C'));
579
		$this->assertEquals('AD',
580
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D'));
581
582
		// test inequalities with simple numbers
583
		$this->assertEquals('ABD', $this->render('A<% if 5 > 3 %>B<% else %>C<% end_if %>D'));
584
		$this->assertEquals('ABD', $this->render('A<% if 5 >= 3 %>B<% else %>C<% end_if %>D'));
585
		$this->assertEquals('ACD', $this->render('A<% if 3 > 5 %>B<% else %>C<% end_if %>D'));
586
		$this->assertEquals('ACD', $this->render('A<% if 3 >= 5 %>B<% else %>C<% end_if %>D'));
587
588
		$this->assertEquals('ABD', $this->render('A<% if 3 < 5 %>B<% else %>C<% end_if %>D'));
589
		$this->assertEquals('ABD', $this->render('A<% if 3 <= 5 %>B<% else %>C<% end_if %>D'));
590
		$this->assertEquals('ACD', $this->render('A<% if 5 < 3 %>B<% else %>C<% end_if %>D'));
591
		$this->assertEquals('ACD', $this->render('A<% if 5 <= 3 %>B<% else %>C<% end_if %>D'));
592
593
		$this->assertEquals('ABD', $this->render('A<% if 4 <= 4 %>B<% else %>C<% end_if %>D'));
594
		$this->assertEquals('ABD', $this->render('A<% if 4 >= 4 %>B<% else %>C<% end_if %>D'));
595
		$this->assertEquals('ACD', $this->render('A<% if 4 > 4 %>B<% else %>C<% end_if %>D'));
596
		$this->assertEquals('ACD', $this->render('A<% if 4 < 4 %>B<% else %>C<% end_if %>D'));
597
598
		// empty else_if and else tags, if this would not be supported,
599
		// the output would stop after A, thereby failing the assert
600
		$this->assertEquals('AD', $this->render('A<% if IsSet %><% else %><% end_if %>D'));
601
		$this->assertEquals('AD',
602
			$this->render('A<% if NotSet %><% else_if IsSet %><% else %><% end_if %>D'));
603
		$this->assertEquals('AD',
604
			$this->render('A<% if NotSet %><% else_if AlsoNotSet %><% else %><% end_if %>D'));
605
606
		// Bare words with ending space
607
		$this->assertEquals('ABC',
608
			$this->render('A<% if "RawVal" == RawVal %>B<% end_if %>C'));
609
610
		// Else
611
		$this->assertEquals('ADE',
612
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E'));
613
614
		// Empty if with else
615
		$this->assertEquals('ABC',
616
			$this->render('A<% if NotSet %><% else %>B<% end_if %>C'));
617
	}
618
619
	public function testBaseTagGeneration() {
620
		// XHTML wil have a closed base tag
621
		$tmpl1 = '<?xml version="1.0" encoding="UTF-8"?>
622
			<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
623
				. ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
624
			<html>
625
				<head><% base_tag %></head>
626
				<body><p>test</p><body>
627
			</html>';
628
		$this->assertRegExp('/<head><base href=".*" \/><\/head>/', $this->render($tmpl1));
629
630
		// HTML4 and 5 will only have it for IE
631
		$tmpl2 = '<!DOCTYPE html>
632
			<html>
633
				<head><% base_tag %></head>
634
				<body><p>test</p><body>
635
			</html>';
636
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
637
			$this->render($tmpl2));
638
639
640
		$tmpl3 = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
641
			<html>
642
				<head><% base_tag %></head>
643
				<body><p>test</p><body>
644
			</html>';
645
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
646
			$this->render($tmpl3));
647
648
		// Check that the content negotiator converts to the equally legal formats
649
		$negotiator = new ContentNegotiator();
650
651
		$response = new SS_HTTPResponse($this->render($tmpl1));
652
		$negotiator->html($response);
653
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
654
			$response->getBody());
655
656
		$response = new SS_HTTPResponse($this->render($tmpl1));
657
		$negotiator->xhtml($response);
658
		$this->assertRegExp('/<head><base href=".*" \/><\/head>/', $response->getBody());
659
	}
660
661
	public function testIncludeWithArguments() {
662
		$this->assertEquals(
663
			$this->render('<% include SSViewerTestIncludeWithArguments %>'),
664
			'<p>[out:Arg1]</p><p>[out:Arg2]</p>'
665
		);
666
667
		$this->assertEquals(
668
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A %>'),
669
			'<p>A</p><p>[out:Arg2]</p>'
670
		);
671
672
		$this->assertEquals(
673
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A, Arg2=B %>'),
674
			'<p>A</p><p>B</p>'
675
		);
676
677
		$this->assertEquals(
678
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'),
679
			'<p>A Bare String</p><p>B Bare String</p>'
680
		);
681
682
		$this->assertEquals(
683
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=$B %>',
684
				new ArrayData(array('B' => 'Bar'))),
685
			'<p>A</p><p>Bar</p>'
686
		);
687
688
		$this->assertEquals(
689
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A" %>',
690
				new ArrayData(array('Arg1' => 'Foo', 'Arg2' => 'Bar'))),
691
			'<p>A</p><p>Bar</p>'
692
		);
693
694
		$this->assertEquals(
695
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>',
696
				new ArrayData(array('Items' => new ArrayList(array(
697
					new ArrayData(array('Title' => 'Foo')),
698
					new ArrayData(array('Title' => 'Bar'))
699
				))))),
700
			'SomeArg - Foo - Bar - SomeArg'
701
		);
702
703
		$this->assertEquals(
704
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInWith Title="A" %>',
705
				new ArrayData(array('Item' => new ArrayData(array('Title' =>'B'))))),
706
			'A - B - A'
707
		);
708
709
		$this->assertEquals(
710
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>',
711
				new ArrayData(array(
712
					'Item' => new ArrayData(array(
713
						'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
714
					)))
715
				)),
716
			'A - B - C - B - A'
717
		);
718
719
		$data = new ArrayData(array(
720
			'Nested' => new ArrayData(array(
721
				'Object' => new ArrayData(array('Key' => 'A'))
722
			)),
723
			'Object' => new ArrayData(array('Key' => 'B'))
724
		));
725
726
		$tmpl = SSViewer::fromString('<% include SSViewerTestIncludeObjectArguments A=$Nested.Object, B=$Object %>');
727
		$res  = $tmpl->process($data);
728
		$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...
729
	}
730
731
	public function testNamespaceInclude() {
732
		$data = new ArrayData([]);
733
734
		$this->assertEquals(
735
			"tests:( NamespaceInclude\n )",
736
			$this->render('tests:( <% include Namespace\NamespaceInclude %> )', $data),
737
			'Backslashes work for namespace references in includes'
738
		);
739
740
		$this->assertEquals(
741
			"tests:( NamespaceInclude\n )",
742
			$this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data),
743
			'Forward slashes work for namespace references in includes'
744
		);
745
746
		$this->assertEquals(
747
			"tests:( NamespaceInclude\n )",
748
			$this->render('tests:( <% include NamespaceInclude %> )', $data),
749
			'Namespace can be missed for a namespaed include'
750
		);
751
	}
752
753
754
	public function testRecursiveInclude() {
755
		$view = new SSViewer(array('SSViewerTestRecursiveInclude'));
756
757
		$data = new ArrayData(array(
758
			'Title' => 'A',
759
			'Children' => new ArrayList(array(
760
				new ArrayData(array(
761
					'Title' => 'A1',
762
					'Children' => new ArrayList(array(
763
						new ArrayData(array( 'Title' => 'A1 i', )),
764
						new ArrayData(array( 'Title' => 'A1 ii', )),
765
					)),
766
				)),
767
				new ArrayData(array( 'Title' => 'A2', )),
768
				new ArrayData(array( 'Title' => 'A3', )),
769
			)),
770
		));
771
772
		$result = $view->process($data);
773
		// We don't care about whitespace
774
		$rationalisedResult = trim(preg_replace('/\s+/', ' ', $result));
775
776
		$this->assertEquals('A A1 A1 i A1 ii A2 A3', $rationalisedResult);
777
	}
778
779
	public function assertEqualIgnoringWhitespace($a, $b) {
780
		$this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b));
781
	}
782
783
	/**
784
	 * See {@link ViewableDataTest} for more extensive casting tests,
785
	 * this test just ensures that basic casting is correctly applied during template parsing.
786
	 */
787
	public function testCastingHelpers() {
788
		$vd = new SSViewerTest_ViewableData();
789
		$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...
790
		$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...
791
		$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...
792
793
		// Value casted as "Text"
794
		$this->assertEquals(
795
			'&lt;b&gt;html&lt;/b&gt;',
796
			$t = SSViewer::fromString('$TextValue')->process($vd)
797
		);
798
		$this->assertEquals(
799
			'<b>html</b>',
800
			$t = SSViewer::fromString('$TextValue.RAW')->process($vd)
801
		);
802
		$this->assertEquals(
803
			'&lt;b&gt;html&lt;/b&gt;',
804
			$t = SSViewer::fromString('$TextValue.XML')->process($vd)
805
		);
806
807
		// Value casted as "HTMLText"
808
		$this->assertEquals(
809
			'<b>html</b>',
810
			$t = SSViewer::fromString('$HTMLValue')->process($vd)
811
		);
812
		$this->assertEquals(
813
			'<b>html</b>',
814
			$t = SSViewer::fromString('$HTMLValue.RAW')->process($vd)
815
		);
816
		$this->assertEquals(
817
			'&lt;b&gt;html&lt;/b&gt;',
818
			$t = SSViewer::fromString('$HTMLValue.XML')->process($vd)
819
		);
820
821
		// Uncasted value (falls back to ViewableData::$default_cast="HTMLText")
822
		$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
823
		$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...
824
		$this->assertEquals(
825
			'<b>html</b>',
826
			$t = SSViewer::fromString('$UncastedValue')->process($vd)
827
		);
828
		$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
829
		$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...
830
		$this->assertEquals(
831
			'<b>html</b>',
832
			$t = SSViewer::fromString('$UncastedValue.RAW')->process($vd)
833
		);
834
		$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
835
		$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...
836
		$this->assertEquals(
837
			'&lt;b&gt;html&lt;/b&gt;',
838
			$t = SSViewer::fromString('$UncastedValue.XML')->process($vd)
839
		);
840
	}
841
842
	public function testSSViewerBasicIteratorSupport() {
843
		$data = new ArrayData(array(
844
			'Set' => new ArrayList(array(
845
				new SSViewerTest_Object("1"),
846
				new SSViewerTest_Object("2"),
847
				new SSViewerTest_Object("3"),
848
				new SSViewerTest_Object("4"),
849
				new SSViewerTest_Object("5"),
850
				new SSViewerTest_Object("6"),
851
				new SSViewerTest_Object("7"),
852
				new SSViewerTest_Object("8"),
853
				new SSViewerTest_Object("9"),
854
				new SSViewerTest_Object("10"),
855
			))
856
		));
857
858
		//base test
859
		$result = $this->render('<% loop Set %>$Number<% end_loop %>',$data);
860
		$this->assertEquals("12345678910",$result,"Numbers rendered in order");
861
862
		//test First
863
		$result = $this->render('<% loop Set %><% if First %>$Number<% end_if %><% end_loop %>',$data);
864
		$this->assertEquals("1",$result,"Only the first number is rendered");
865
866
		//test Last
867
		$result = $this->render('<% loop Set %><% if Last %>$Number<% end_if %><% end_loop %>',$data);
868
		$this->assertEquals("10",$result,"Only the last number is rendered");
869
870
		//test Even
871
		$result = $this->render('<% loop Set %><% if Even() %>$Number<% end_if %><% end_loop %>',$data);
872
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
873
874
		//test Even with quotes
875
		$result = $this->render('<% loop Set %><% if Even("1") %>$Number<% end_if %><% end_loop %>',$data);
876
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
877
878
		//test Even without quotes
879
		$result = $this->render('<% loop Set %><% if Even(1) %>$Number<% end_if %><% end_loop %>',$data);
880
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
881
882
		//test Even with zero-based start index
883
		$result = $this->render('<% loop Set %><% if Even("0") %>$Number<% end_if %><% end_loop %>',$data);
884
		$this->assertEquals("13579",$result,"Even (with zero-based index) numbers rendered in order");
885
886
		//test Odd
887
		$result = $this->render('<% loop Set %><% if Odd %>$Number<% end_if %><% end_loop %>',$data);
888
		$this->assertEquals("13579",$result,"Odd numbers rendered in order");
889
890
		//test FirstLast
891
		$result = $this->render('<% loop Set %><% if FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>',$data);
892
		$this->assertEquals("1first10last",$result,"First and last numbers rendered in order");
893
894
		//test Middle
895
		$result = $this->render('<% loop Set %><% if Middle %>$Number<% end_if %><% end_loop %>',$data);
896
		$this->assertEquals("23456789",$result,"Middle numbers rendered in order");
897
898
		//test MiddleString
899
		$result = $this->render('<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %>'
900
			. '<% end_loop %>',$data);
901
		$this->assertEquals("2middle3middle4middle5middle6middle7middle8middle9middle",$result,
902
			"Middle numbers rendered in order");
903
904
		//test EvenOdd
905
		$result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>',$data);
906
		$this->assertEquals("oddevenoddevenoddevenoddevenoddeven",$result,
907
			"Even and Odd is returned in sequence numbers rendered in order");
908
909
		//test Pos
910
		$result = $this->render('<% loop Set %>$Pos<% end_loop %>',$data);
911
		$this->assertEquals("12345678910", $result, '$Pos is rendered in order');
912
913
		//test Pos
914
		$result = $this->render('<% loop Set %>$Pos(0)<% end_loop %>',$data);
915
		$this->assertEquals("0123456789", $result, '$Pos(0) is rendered in order');
916
917
		//test FromEnd
918
		$result = $this->render('<% loop Set %>$FromEnd<% end_loop %>',$data);
919
		$this->assertEquals("10987654321", $result, '$FromEnd is rendered in order');
920
921
		//test FromEnd
922
		$result = $this->render('<% loop Set %>$FromEnd(0)<% end_loop %>',$data);
923
		$this->assertEquals("9876543210", $result, '$FromEnd(0) rendered in order');
924
925
		//test Total
926
		$result = $this->render('<% loop Set %>$TotalItems<% end_loop %>',$data);
927
		$this->assertEquals("10101010101010101010",$result,"10 total items X 10 are returned");
928
929
		//test Modulus
930
		$result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>',$data);
931
		$this->assertEquals("1010101010",$result,"1-indexed pos modular divided by 2 rendered in order");
932
933
		//test MultipleOf 3
934
		$result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>',$data);
935
		$this->assertEquals("369",$result,"Only numbers that are multiples of 3 are returned");
936
937
		//test MultipleOf 4
938
		$result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>',$data);
939
		$this->assertEquals("48",$result,"Only numbers that are multiples of 4 are returned");
940
941
		//test MultipleOf 5
942
		$result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>',$data);
943
		$this->assertEquals("510",$result,"Only numbers that are multiples of 5 are returned");
944
945
		//test MultipleOf 10
946
		$result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>',$data);
947
		$this->assertEquals("10",$result,"Only numbers that are multiples of 10 (with 1-based indexing) are returned");
948
949
		//test MultipleOf 9 zero-based
950
		$result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>',$data);
951
		$this->assertEquals("110",$result,
952
			"Only numbers that are multiples of 9 with zero-based indexing are returned. (The first and last item)");
953
954
		//test MultipleOf 11
955
		$result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>',$data);
956
		$this->assertEquals("",$result,"Only numbers that are multiples of 11 are returned. I.e. nothing returned");
957
	}
958
959
	/**
960
	 * Test $Up works when the scope $Up refers to was entered with a "with" block
961
	 */
962
	public function testUpInWith() {
963
964
		// Data to run the loop tests on - three levels deep
965
		$data = new ArrayData(array(
966
			'Name' => 'Top',
967
			'Foo' => new ArrayData(array(
968
				'Name' => 'Foo',
969
				'Bar' => new ArrayData(array(
970
					'Name' => 'Bar',
971
					'Baz' => new ArrayData(array(
972
						'Name' => 'Baz'
973
					)),
974
					'Qux' => new ArrayData(array(
975
						'Name' => 'Qux'
976
					))
977
				))
978
			))
979
		));
980
981
		// Basic functionality
982
		$this->assertEquals('BarFoo',
983
			$this->render('<% with Foo %><% with Bar %>{$Name}{$Up.Name}<% end_with %><% end_with %>', $data));
984
985
		// Two level with block, up refers to internally referenced Bar
986
		$this->assertEquals('BarFoo',
987
			$this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data));
988
989
		// Stepping up & back down the scope tree
990
		$this->assertEquals('BazBarQux',
991
			$this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Name}{$Up.Qux.Name}<% end_with %>', $data));
992
993
		// Using $Up in a with block
994
		$this->assertEquals('BazBarQux',
995
			$this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Qux.Name}<% end_with %>'
996
				.'<% end_with %>', $data));
997
998
		// Stepping up & back down the scope tree with with blocks
999
		$this->assertEquals('BazBarQuxBarBaz',
1000
			$this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Qux %>{$Name}<% end_with %>'
1001
				. '{$Name}<% end_with %>{$Name}<% end_with %>', $data));
1002
1003
		// Using $Up.Up, where first $Up points to a previous scope entered using $Up, thereby skipping up to Foo
1004
		$this->assertEquals('Foo',
1005
			$this->render('<% with Foo.Bar.Baz %><% with Up %><% with Qux %>{$Up.Up.Name}<% end_with %><% end_with %>'
1006
				. '<% end_with %>', $data));
1007
1008
		// Using $Up.Up, where first $Up points to an Up used in a local scope lookup, should still skip to Foo
1009
		$this->assertEquals('Foo',
1010
			$this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Up.Name}<% end_with %>', $data));
1011
	}
1012
1013
	/**
1014
	 * Test $Up works when the scope $Up refers to was entered with a "loop" block
1015
	 */
1016
	public function testUpInLoop(){
1017
1018
		// Data to run the loop tests on - one sequence of three items, each with a subitem
1019
		$data = new ArrayData(array(
1020
			'Name' => 'Top',
1021
			'Foo' => new ArrayList(array(
1022
				new ArrayData(array(
1023
					'Name' => '1',
1024
					'Sub' => new ArrayData(array(
1025
						'Name' => 'Bar'
1026
					))
1027
				)),
1028
				new ArrayData(array(
1029
					'Name' => '2',
1030
					'Sub' => new ArrayData(array(
1031
						'Name' => 'Baz'
1032
					))
1033
				)),
1034
				new ArrayData(array(
1035
					'Name' => '3',
1036
					'Sub' => new ArrayData(array(
1037
						'Name' => 'Qux'
1038
					))
1039
				))
1040
			))
1041
		));
1042
1043
		// Make sure inside a loop, $Up refers to the current item of the loop
1044
		$this->assertEqualIgnoringWhitespace(
1045
			'111 222 333',
1046
			$this->render(
1047
				'<% loop $Foo %>$Name<% with $Sub %>$Up.Name<% end_with %>$Name<% end_loop %>',
1048
				$data
1049
			)
1050
		);
1051
1052
		// Make sure inside a loop, looping over $Up uses a separate iterator,
1053
		// and doesn't interfere with the original iterator
1054
		$this->assertEqualIgnoringWhitespace(
1055
			'1Bar123Bar1 2Baz123Baz2 3Qux123Qux3',
1056
			$this->render(
1057
				'<% loop $Foo %>
1058
					$Name
1059
					<% with $Sub %>
1060
						$Name
1061
						<% loop $Up %>$Name<% end_loop %>
1062
						$Name
1063
					<% end_with %>
1064
					$Name
1065
				<% end_loop %>',
1066
				$data
1067
			)
1068
		);
1069
1070
		// Make sure inside a loop, looping over $Up uses a separate iterator,
1071
		// and doesn't interfere with the original iterator or local lookups
1072
		$this->assertEqualIgnoringWhitespace(
1073
			'1 Bar1 123 1Bar 1   2 Baz2 123 2Baz 2   3 Qux3 123 3Qux 3',
1074
			$this->render(
1075
				'<% loop $Foo %>
1076
					$Name
1077
					<% with $Sub %>
1078
						{$Name}{$Up.Name}
1079
						<% loop $Up %>$Name<% end_loop %>
1080
						{$Up.Name}{$Name}
1081
					<% end_with %>
1082
					$Name
1083
				<% end_loop %>',
1084
				$data
1085
			)
1086
		);
1087
	}
1088
1089
	/**
1090
	 * Test that nested loops restore the loop variables correctly when pushing and popping states
1091
	 */
1092
	public function testNestedLoops(){
1093
1094
		// Data to run the loop tests on - one sequence of three items, one with child elements
1095
		// (of a different size to the main sequence)
1096
		$data = new ArrayData(array(
1097
			'Foo' => new ArrayList(array(
1098
				new ArrayData(array(
1099
					'Name' => '1',
1100
					'Children' => new ArrayList(array(
1101
						new ArrayData(array(
1102
							'Name' => 'a'
1103
						)),
1104
						new ArrayData(array(
1105
							'Name' => 'b'
1106
						)),
1107
					)),
1108
				)),
1109
				new ArrayData(array(
1110
					'Name' => '2',
1111
					'Children' => new ArrayList(),
1112
				)),
1113
				new ArrayData(array(
1114
					'Name' => '3',
1115
					'Children' => new ArrayList(),
1116
				)),
1117
			)),
1118
		));
1119
1120
		// Make sure that including a loop inside a loop will not destroy the internal count of
1121
		// items, checked by using "Last"
1122
		$this->assertEqualIgnoringWhitespace(
1123
			'1ab23last',
1124
			$this->render('<% loop $Foo %>$Name<% loop Children %>$Name<% end_loop %><% if Last %>last<% end_if %>'
1125
				. '<% end_loop %>', $data
1126
			)
1127
		);
1128
	}
1129
1130
	public function testLayout() {
1131
		$self = $this;
1132
1133
		$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
1134
			$template = new SSViewer(array('Page'));
1135
			$self->assertEquals("Foo\n\n", $template->process(new ArrayData(array())));
1136
1137
			$template = new SSViewer(array('Shortcodes', 'Page'));
1138
			$self->assertEquals("[file_link]\n\n", $template->process(new ArrayData(array())));
1139
		});
1140
	}
1141
1142
	/**
1143
	 * @covers SSViewer::get_templates_by_class()
1144
	 */
1145
	public function testGetTemplatesByClass() {
1146
		$self = $this;
1147
		$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
1148
			// Test passing a string
1149
			$templates = SSViewer::get_templates_by_class(
1150
				'TestNamespace\SSViewerTest_Controller',
1151
				'',
1152
				'Controller'
1153
			);
1154
			$self->assertEquals([
1155
				'TestNamespace\SSViewerTest_Controller',
1156
    			'Controller',
1157
			], $templates);
1158
1159
			// Test to ensure we're stopping at the base class.
1160
			$templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTest_Controller', '', 'TestNamespace\SSViewerTest_Controller');
1161
			$self->assertCount(1, $templates);
1162
1163
			// Make sure we can filter our templates by suffix.
1164
			$templates = SSViewer::get_templates_by_class('SSViewerTest', '_Controller');
1165
			$self->assertCount(1, $templates);
1166
1167
			// Let's throw something random in there.
1168
			$self->setExpectedException('InvalidArgumentException');
1169
			$templates = SSViewer::get_templates_by_class(array());
0 ignored issues
show
Unused Code introduced by
$templates is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1170
		});
1171
	}
1172
1173
	/**
1174
	 * @covers SSViewer::get_themes()
1175
	 */
1176
	public function testThemeRetrieval() {
1177
		$ds = DIRECTORY_SEPARATOR;
1178
		$testThemeBaseDir = TEMP_FOLDER . $ds . 'test-themes';
1179
1180
		if(file_exists($testThemeBaseDir)) Filesystem::removeFolder($testThemeBaseDir);
1181
1182
		mkdir($testThemeBaseDir);
1183
		mkdir($testThemeBaseDir . $ds . 'blackcandy');
1184
		mkdir($testThemeBaseDir . $ds . 'blackcandy_blog');
1185
		mkdir($testThemeBaseDir . $ds . 'darkshades');
1186
		mkdir($testThemeBaseDir . $ds . 'darkshades_blog');
1187
1188
		$this->assertEquals(array(
1189
			'blackcandy' => 'blackcandy',
1190
			'darkshades' => 'darkshades'
1191
		), SSViewer::get_themes($testThemeBaseDir), 'Our test theme directory contains 2 themes');
1192
1193
		$this->assertEquals(array(
1194
			'blackcandy' => 'blackcandy',
1195
			'blackcandy_blog' => 'blackcandy_blog',
1196
			'darkshades' => 'darkshades',
1197
			'darkshades_blog' => 'darkshades_blog'
1198
		), SSViewer::get_themes($testThemeBaseDir, true),
1199
			'Our test theme directory contains 2 themes and 2 sub-themes');
1200
1201
		// Remove all the test themes we created
1202
		Filesystem::removeFolder($testThemeBaseDir);
1203
	}
1204
1205
	public function testRewriteHashlinks() {
1206
		$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1207
		Config::inst()->update('SSViewer', 'rewrite_hash_links', true);
1208
1209
		$_SERVER['HTTP_HOST'] = 'www.mysite.com';
1210
		$_SERVER['REQUEST_URI'] = '//file.com?foo"onclick="alert(\'xss\')""';
1211
1212
		// Emulate SSViewer::process()
1213
		// Note that leading double slashes have been rewritten to prevent these being mis-interepreted
1214
		// as protocol-less absolute urls
1215
		$base = Convert::raw2att('/file.com?foo"onclick="alert(\'xss\')""');
1216
1217
		$tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinks_' . sha1(rand()) . '.ss';
1218
1219
		// Note: SSViewer_FromString doesn't rewrite hash links.
1220
		file_put_contents($tmplFile, '<!DOCTYPE html>
1221
			<html>
1222
				<head><% base_tag %></head>
1223
				<body>
1224
				<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>
1225
				$ExternalInsertedLink
1226
				<a class="inline" href="#anchor">InlineLink</a>
1227
				$InsertedLink
1228
				<svg><use xlink:href="#sprite"></use></svg>
1229
				<body>
1230
			</html>');
1231
		$tmpl = new SSViewer($tmplFile);
1232
		$obj = new ViewableData();
1233
		$obj->InsertedLink = '<a class="inserted" href="#anchor">InsertedLink</a>';
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...
1234
		$obj->ExternalInsertedLink = '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>';
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...
1235
		$result = $tmpl->process($obj);
1236
		$this->assertContains(
1237
			'<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
1238
			$result
1239
		);
1240
		$this->assertContains(
1241
			'<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>',
1242
			$result
1243
		);
1244
		$this->assertContains(
1245
			'<a class="inline" href="' . $base . '#anchor">InlineLink</a>',
1246
			$result
1247
		);
1248
		$this->assertContains(
1249
			'<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>',
1250
			$result
1251
		);
1252
		$this->assertContains(
1253
			'<svg><use xlink:href="#sprite"></use></svg>',
1254
			$result,
1255
			'SSTemplateParser should only rewrite anchor hrefs'
1256
		);
1257
1258
		unlink($tmplFile);
1259
1260
		Config::inst()->update('SSViewer', 'rewrite_hash_links', $orig);
1261
	}
1262
1263
	public function testRewriteHashlinksInPhpMode() {
1264
		$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1265
		Config::inst()->update('SSViewer', 'rewrite_hash_links', 'php');
1266
1267
		$tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinksInPhpMode_' . sha1(rand()) . '.ss';
1268
1269
		// Note: SSViewer_FromString doesn't rewrite hash links.
1270
		file_put_contents($tmplFile, '<!DOCTYPE html>
1271
			<html>
1272
				<head><% base_tag %></head>
1273
				<body>
1274
				<a class="inline" href="#anchor">InlineLink</a>
1275
				$InsertedLink
1276
				<svg><use xlink:href="#sprite"></use></svg>
1277
				<body>
1278
			</html>');
1279
		$tmpl = new SSViewer($tmplFile);
1280
		$obj = new ViewableData();
1281
		$obj->InsertedLink = '<a class="inserted" href="#anchor">InsertedLink</a>';
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...
1282
		$result = $tmpl->process($obj);
1283
1284
		$code = <<<'EOC'
1285
<a class="inserted" href="<?php echo Convert::raw2att(preg_replace("/^(\/)+/", "/", $_SERVER['REQUEST_URI'])); ?>#anchor">InsertedLink</a>
1286
EOC;
1287
		$this->assertContains($code, $result);
1288
		// TODO Fix inline links in PHP mode
1289
		// $this->assertContains(
1290
		// 	'<a class="inline" href="<?php echo str_replace(',
1291
		// 	$result
1292
		// );
1293
		$this->assertContains(
1294
			'<svg><use xlink:href="#sprite"></use></svg>',
1295
			$result,
1296
			'SSTemplateParser should only rewrite anchor hrefs'
1297
		);
1298
1299
		unlink($tmplFile);
1300
1301
		Config::inst()->update('SSViewer', 'rewrite_hash_links', $orig);
1302
	}
1303
1304
	public function testRenderWithSourceFileComments() {
1305
		$origEnv = Config::inst()->get('Director', 'environment_type');
1306
		Config::inst()->update('Director', 'environment_type', 'dev');
1307
		Config::inst()->update('SSViewer', 'source_file_comments', true);
1308
		$f = FRAMEWORK_PATH . '/tests/templates/SSViewerTestComments';
1309
		$templates = array(
1310
			array(
1311
				'name' => 'SSViewerTestCommentsFullSource',
1312
				'expected' => ""
1313
					. "<!doctype html>"
1314
					. "<!-- template $f/SSViewerTestCommentsFullSource.ss -->"
1315
					. "<html>"
1316
					. "\t<head></head>"
1317
					. "\t<body></body>"
1318
					. "</html>"
1319
					. "<!-- end template $f/SSViewerTestCommentsFullSource.ss -->",
1320
			),
1321
			array(
1322
				'name' => 'SSViewerTestCommentsFullSourceHTML4Doctype',
1323
				'expected' => ""
1324
					. "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML "
1325
					. "4.01//EN\"\t\t\"http://www.w3.org/TR/html4/strict.dtd\">"
1326
					. "<!-- template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->"
1327
					. "<html>"
1328
					. "\t<head></head>"
1329
					. "\t<body></body>"
1330
					. "</html>"
1331
					. "<!-- end template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->",
1332
			),
1333
			array(
1334
				'name' => 'SSViewerTestCommentsFullSourceNoDoctype',
1335
				'expected' => ""
1336
					. "<html><!-- template $f/SSViewerTestCommentsFullSourceNoDoctype.ss -->"
1337
					. "\t<head></head>"
1338
					. "\t<body></body>"
1339
					. "<!-- end template $f/SSViewerTestCommentsFullSourceNoDoctype.ss --></html>",
1340
			),
1341
			array(
1342
				'name' => 'SSViewerTestCommentsFullSourceIfIE',
1343
				'expected' => ""
1344
					. "<!doctype html>"
1345
					. "<!-- template $f/SSViewerTestCommentsFullSourceIfIE.ss -->"
1346
					. "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1347
					. "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1348
					. "<!--[if !IE]><!--> <html class='no-ie'> <!--<![endif]-->"
1349
					. "\t<head></head>"
1350
					. "\t<body></body>"
1351
					. "</html>"
1352
					. "<!-- end template $f/SSViewerTestCommentsFullSourceIfIE.ss -->",
1353
			),
1354
			array(
1355
				'name' => 'SSViewerTestCommentsFullSourceIfIENoDoctype',
1356
				'expected' => ""
1357
					. "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1358
					. "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1359
					. "<!--[if !IE]><!--> <html class='no-ie'>"
1360
					. "<!-- template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss -->"
1361
					. " <!--<![endif]-->"
1362
					. "\t<head></head>"
1363
					. "\t<body></body>"
1364
					. "<!-- end template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss --></html>",
1365
			),
1366
			array(
1367
				'name' => 'SSViewerTestCommentsPartialSource',
1368
				'expected' =>
1369
				"<!-- template $f/SSViewerTestCommentsPartialSource.ss -->"
1370
					. "<div class='typography'></div>"
1371
					. "<!-- end template $f/SSViewerTestCommentsPartialSource.ss -->",
1372
			),
1373
			array(
1374
				'name' => 'SSViewerTestCommentsWithInclude',
1375
				'expected' =>
1376
				"<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
1377
					. "<div class='typography'>"
1378
					. "<!-- include 'SSViewerTestCommentsInclude' -->"
1379
					. "<!-- template $f/SSViewerTestCommentsInclude.ss -->"
1380
					. "Included"
1381
					. "<!-- end template $f/SSViewerTestCommentsInclude.ss -->"
1382
					. "<!-- end include 'SSViewerTestCommentsInclude' -->"
1383
					. "</div>"
1384
					. "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
1385
			),
1386
		);
1387
		foreach ($templates as $template) {
1388
			$this->_renderWithSourceFileComments($template['name'], $template['expected']);
1389
		}
1390
		Config::inst()->update('SSViewer', 'source_file_comments', false);
1391
		Config::inst()->update('Director', 'environment_type', $origEnv);
1392
	}
1393
	private function _renderWithSourceFileComments($name, $expected) {
1394
		$viewer = new SSViewer(array($name));
1395
		$data = new ArrayData(array());
1396
		$result = $viewer->process($data);
1397
		$expected = str_replace(array("\r", "\n"), '', $expected);
1398
		$result = str_replace(array("\r", "\n"), '', $result);
1399
		$this->assertEquals($result, $expected);
1400
	}
1401
1402
	public function testLoopIteratorIterator() {
1403
		$list = new PaginatedList(new ArrayList());
1404
		$viewer = new SSViewer_FromString('<% loop List %>$ID - $FirstName<br /><% end_loop %>');
1405
		$result = $viewer->process(new ArrayData(array('List' => $list)));
1406
		$this->assertEquals($result, '');
1407
	}
1408
1409
	public function testProcessOnlyIncludesRequirementsOnce() {
1410
		$template = new SSViewer(array('SSViewerTestProcess'));
1411
		$basePath = dirname($this->getCurrentRelativePath()) . '/forms';
1412
1413
		$backend = Injector::inst()->create('Requirements_Backend');
1414
		$backend->setCombinedFilesEnabled(false);
1415
		$backend->combineFiles(
1416
			'RequirementsTest_ab.css',
1417
			array(
1418
				$basePath . '/RequirementsTest_a.css',
1419
				$basePath . '/RequirementsTest_b.css'
1420
			)
1421
		);
1422
1423
		Requirements::set_backend($backend);
1424
1425
		$this->assertEquals(1, substr_count($template->process(array()), "a.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1426
		$this->assertEquals(1, substr_count($template->process(array()), "b.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1427
1428
		// if we disable the requirements then we should get nothing
1429
		$template->includeRequirements(false);
1430
		$this->assertEquals(0, substr_count($template->process(array()), "a.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1431
		$this->assertEquals(0, substr_count($template->process(array()), "b.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1432
	}
1433
1434
	public function testRequireCallInTemplateInclude() {
1435
		//TODO undo skip test on the event that templates ever obtain the ability to reference MODULE_DIR (or something to that effect)
1436
		if(FRAMEWORK_DIR === 'framework') {
1437
			$template = new SSViewer(array('SSViewerTestProcess'));
1438
1439
			Requirements::set_suffix_requirements(false);
1440
1441
			$this->assertEquals(1, substr_count(
1442
				$template->process(array()),
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1443
				"tests/forms/RequirementsTest_a.js"
1444
			));
1445
		}
1446
		else {
1447
			$this->markTestSkipped('Requirement will always fail if the framework dir is not '.
1448
				'named \'framework\', since templates require hard coded paths');
1449
		}
1450
	}
1451
1452
	public function testCallsWithArguments() {
1453
		$data = new ArrayData(array(
1454
			'Set' => new ArrayList(array(
1455
				new SSViewerTest_Object("1"),
1456
				new SSViewerTest_Object("2"),
1457
				new SSViewerTest_Object("3"),
1458
				new SSViewerTest_Object("4"),
1459
				new SSViewerTest_Object("5"),
1460
			)),
1461
			'Level' => new SSViewerTest_LevelTest(1),
1462
			'Nest' => array(
1463
				'Level' => new SSViewerTest_LevelTest(2),
1464
			),
1465
		));
1466
1467
		$tests = array(
1468
			'$Level.output(1)' => '1-1',
1469
			'$Nest.Level.output($Set.First.Number)' => '2-1',
1470
			'<% with $Set %>$Up.Level.output($First.Number)<% end_with %>' => '1-1',
1471
			'<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>' => '2-1',
1472
			'<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>' => '2-12-22-32-42-5',
1473
			'<% loop $Set %>$Top.Level.output($Number)<% end_loop %>' => '1-11-21-31-41-5',
1474
			'<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>' => '2-1',
1475
			'<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>' => '1-5',
1476
			'<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>' => '5-hi',
1477
			'<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>' => '!0',
1478
			'<% with $Nest %>
1479
				<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
1480
			<% end_with %>' => '1-hi',
1481
			'<% with $Nest %>
1482
				<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
1483
			<% end_with %>' => '!0!1!2!3!4',
1484
		);
1485
1486
		foreach($tests as $template => $expected) {
1487
			$this->assertEquals($expected, trim($this->render($template, $data)));
1488
		}
1489
	}
1490
1491 View Code Duplication
	public function testClosedBlockExtension() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1492
		$count = 0;
1493
		$parser = new SSTemplateParser();
1494
		$parser->addClosedBlock(
1495
			'test',
1496
			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...
1497
				$count++;
1498
			}
1499
		);
1500
1501
		$template = new SSViewer_FromString("<% test %><% end_test %>", $parser);
1502
		$template->process(new SSViewerTestFixture());
1503
1504
		$this->assertEquals(1, $count);
1505
	}
1506
1507 View Code Duplication
	public function testOpenBlockExtension() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1508
		$count = 0;
1509
		$parser = new SSTemplateParser();
1510
		$parser->addOpenBlock(
1511
			'test',
1512
			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...
1513
				$count++;
1514
			}
1515
		);
1516
1517
		$template = new SSViewer_FromString("<% test %>", $parser);
1518
		$template->process(new SSViewerTestFixture());
1519
1520
		$this->assertEquals(1, $count);
1521
	}
1522
1523
	/**
1524
	 * Tests if caching for SSViewer_FromString is working
1525
	 */
1526
	public function testFromStringCaching() {
1527
		$content = 'Test content';
1528
		$cacheFile = TEMP_FOLDER . '/.cache.' . sha1($content);
1529
		if (file_exists($cacheFile)) {
1530
			unlink($cacheFile);
1531
		}
1532
1533
		// Test global behaviors
1534
		$this->render($content, null, null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1535
		$this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
1536
1537
		Config::inst()->update('SSViewer_FromString', 'cache_template', true);
1538
		$this->render($content, null, null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1539
		$this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
1540
		unlink($cacheFile);
1541
1542
		// Test instance behaviors
1543
		$this->render($content, null, false);
1544
		$this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
1545
1546
		$this->render($content, null, true);
1547
		$this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
1548
		unlink($cacheFile);
1549
	}
1550
}
1551
1552
/**
1553
 * A test fixture that will echo back the template item
1554
 */
1555
class SSViewerTestFixture extends ViewableData {
1556
	protected $name;
1557
1558
	public function __construct($name = null) {
1559
		$this->name = $name;
1560
		parent::__construct();
1561
	}
1562
1563
1564
	private function argedName($fieldName, $arguments) {
1565
		$childName = $this->name ? "$this->name.$fieldName" : $fieldName;
1566
		if($arguments) return $childName . '(' . implode(',', $arguments) . ')';
1567
		else return $childName;
1568
	}
1569
	public function obj($fieldName, $arguments=null, $forceReturnedObject=true, $cache=false, $cacheName=null) {
1570
		$childName = $this->argedName($fieldName, $arguments);
1571
1572
		// Special field name Loop### to create a list
1573
		if(preg_match('/^Loop([0-9]+)$/', $fieldName, $matches)) {
1574
			$output = new ArrayList();
1575
			for($i=0;$i<$matches[1];$i++) $output->push(new SSViewerTestFixture($childName));
1576
			return $output;
1577
1578
		} else if(preg_match('/NotSet/i', $fieldName)) {
1579
			return new ViewableData();
1580
1581
		} else {
1582
			return new SSViewerTestFixture($childName);
1583
		}
1584
	}
1585
1586
1587
	public function XML_val($fieldName, $arguments = null, $cache = false) {
1588
		if(preg_match('/NotSet/i', $fieldName)) {
1589
			return '';
1590
		} else if(preg_match('/Raw/i', $fieldName)) {
1591
			return $fieldName;
1592
		} else {
1593
			return '[out:' . $this->argedName($fieldName, $arguments) . ']';
1594
		}
1595
	}
1596
1597
	public function hasValue($fieldName, $arguments = null, $cache = true) {
1598
		return (bool)$this->XML_val($fieldName, $arguments);
1599
	}
1600
}
1601
1602
class SSViewerTest_ViewableData extends ViewableData implements TestOnly {
1603
1604
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
1605
		'TextValue' => 'Text',
1606
		'HTMLValue' => 'HTMLText'
1607
	);
1608
1609
	public function methodWithOneArgument($arg1) {
1610
		return "arg1:{$arg1}";
1611
	}
1612
1613
	public function methodWithTwoArguments($arg1, $arg2) {
1614
		return "arg1:{$arg1},arg2:{$arg2}";
1615
	}
1616
}
1617
1618
class SSViewerTest_Object extends DataObject implements TestOnly {
1619
1620
	public $number = null;
1621
1622
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
1623
		'Link' => 'Text',
1624
	);
1625
1626
1627
	public function __construct($number = null) {
1628
		parent::__construct();
1629
		$this->number = $number;
1630
	}
1631
1632
	public function Number() {
1633
		return $this->number;
1634
	}
1635
1636
	public function absoluteBaseURL() {
1637
		return "testLocalFunctionPriorityCalled";
1638
	}
1639
1640
	public function lotsOfArguments11($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k) {
1641
		return $a. $b. $c. $d. $e. $f. $g. $h. $i. $j. $k;
1642
	}
1643
1644
	public function Link() {
1645
		return 'some/url.html';
1646
	}
1647
}
1648
1649
class SSViewerTest_GlobalProvider implements TemplateGlobalProvider, TestOnly {
1650
1651
	public static function get_template_global_variables() {
1652
		return array(
1653
			'SSViewerTest_GlobalHTMLFragment' => array('method' => 'get_html', 'casting' => 'HTMLText'),
1654
			'SSViewerTest_GlobalHTMLEscaped' => array('method' => 'get_html'),
1655
1656
			'SSViewerTest_GlobalAutomatic',
1657
			'SSViewerTest_GlobalReferencedByString' => 'get_reference',
1658
			'SSViewerTest_GlobalReferencedInArray' => array('method' => 'get_reference'),
1659
1660
			'SSViewerTest_GlobalThatTakesArguments' => array('method' => 'get_argmix', 'casting' => 'HTMLText')
1661
1662
		);
1663
	}
1664
1665
	public static function get_html() {
1666
		return '<div></div>';
1667
	}
1668
1669
	public static function SSViewerTest_GlobalAutomatic() {
1670
		return 'automatic';
1671
	}
1672
1673
	public static function get_reference() {
1674
		return 'reference';
1675
	}
1676
1677
	public static function get_argmix() {
1678
		$args = func_get_args();
1679
		return 'z' . implode(':', $args) . 'z';
1680
	}
1681
1682
}
1683
1684
class SSViewerTest_LevelTest extends ViewableData implements TestOnly {
1685
	protected $depth;
1686
1687
	public function __construct($depth = 1) {
1688
		$this->depth = $depth;
1689
	}
1690
1691
	public function output($val) {
1692
		return "$this->depth-$val";
1693
	}
1694
1695
	public function forLoop($number) {
1696
		$ret = array();
1697
		for($i = 0; $i < (int)$number; ++$i) {
1698
			$ret[] = new SSViewerTest_Object("!$i");
1699
		}
1700
		return new ArrayList($ret);
1701
	}
1702
1703
	public function forWith($number) {
1704
		return new self($number);
1705
	}
1706
}
1707