diff --git a/tests/_files/assets/foo.css b/tests/_files/assets/foo.css new file mode 100644 index 0000000..e69de29 diff --git a/tests/_files/assets/foo.jpg b/tests/_files/assets/foo.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tests/_files/assets/foo.js b/tests/_files/assets/foo.js new file mode 100644 index 0000000..e69de29 diff --git a/tests/_files/bar/foo.php b/tests/_files/bar/foo.php new file mode 100644 index 0000000..e69de29 diff --git a/tests/_files/bar/second.php b/tests/_files/bar/second.php new file mode 100644 index 0000000..88dc436 --- /dev/null +++ b/tests/_files/bar/second.php @@ -0,0 +1,16 @@ +layout('main', ['foo' => 'Bar!']) ?> + +section('one') ?> +World foo ?> +stop() ?> + +section('two') ?> +I Win +replace() ?> + +Buffalo Bill + +section('three') ?> +MAN +stop(); diff --git a/tests/_files/foo/alias.php b/tests/_files/foo/alias.php new file mode 100644 index 0000000..8251678 --- /dev/null +++ b/tests/_files/foo/alias.php @@ -0,0 +1 @@ +foo ?> \ No newline at end of file diff --git a/tests/_files/foo/bar.inc b/tests/_files/foo/bar.inc new file mode 100644 index 0000000..3d6365d --- /dev/null +++ b/tests/_files/foo/bar.inc @@ -0,0 +1,2 @@ +data()); diff --git a/tests/_files/foo/blocks.php b/tests/_files/foo/blocks.php new file mode 100644 index 0000000..d822e67 --- /dev/null +++ b/tests/_files/foo/blocks.php @@ -0,0 +1,11 @@ +block('spaceless') ?> + block('wrap', '
', '
') ?> + block('spaceless') ?> + + endblock('spaceless') ?> + endblock('wrap') ?> +endblock('spaceless') ?> diff --git a/tests/_files/foo/double.tpl.php b/tests/_files/foo/double.tpl.php new file mode 100644 index 0000000..8a41cc6 --- /dev/null +++ b/tests/_files/foo/double.tpl.php @@ -0,0 +1 @@ +I have 2 extensions diff --git a/tests/_files/foo/foo.php b/tests/_files/foo/foo.php new file mode 100644 index 0000000..b8be172 --- /dev/null +++ b/tests/_files/foo/foo.php @@ -0,0 +1,2 @@ +data()); diff --git a/tests/_files/foo/main.php b/tests/_files/foo/main.php new file mode 100644 index 0000000..a3c7316 --- /dev/null +++ b/tests/_files/foo/main.php @@ -0,0 +1,14 @@ +section('one') ?> +Hello foo.' ' ?> +stop() ?> + +Alone + +section('two') ?> +NO +stop() ?> + +section('three') ?> +YES +stop(); diff --git a/tests/_files/stubs.php b/tests/_files/stubs.php new file mode 100644 index 0000000..78f0919 --- /dev/null +++ b/tests/_files/stubs.php @@ -0,0 +1,49 @@ +value = $value; + } +} + +class ToArray extends Value +{ + public function toArray() + { + return ['toarray' => (array) $this->value]; + } +} + +class AsArray extends Value +{ + public function asArray() + { + return ['asarray' => (array) $this->value]; + } +} + +class Json implements JsonSerializable +{ + public function jsonSerialize() + { + return 'I am JSON'; + } +} + +class Target extends Value +{ +} + +class Transformer +{ + public function transform($object) + { + return is_object($object) ? ['transformed' => get_object_vars($object)] : false; + } +} diff --git a/tests/_files/templates/expected.html b/tests/_files/templates/expected.html new file mode 100644 index 0000000..e7939fd --- /dev/null +++ b/tests/_files/templates/expected.html @@ -0,0 +1,113 @@ + + + + + Foil is Awesome! + + + + + +
+ +
+ +
+ +

Deep var test: Lorem Ipsum Dolor

+ +

Default test: I am a default.

+ +

__get test: TEST ME!

+ +

Raw test: This is a strong tag html content

+ +

Autoescape test: <strong>This is a strong tag html content</strong>

+ +

Filter test: I AM LOWERCASE

+ +

Advanced test: HELLO WORLD!

+ +
+ +
+ Here something for you. +
+ +
+

OVERRIDE a section defined in a PARTIAL

+ +

+ I am a partial. And this: "TEST ME!" is a var I share with template, but + this "I am a partial var!" is a specific partial var. +

+ +
+ +
+

+ I am another partial. + This: "I am a partial var too." is a specific partial var. + This: "TEST ME!" is a var I share with template. + A shared var skipped: "Foil is Awesome!". +

+
+ +
+

Should BE SUPPLYED by layout.

+
+ +
+ I am here. +
+ +
+ +

MAIN LAYOUT, first section

+ +
+ +

Should REPLACE layout first-child layout section.

+ +
+ +

MAIN LAYOUT, first section after child

+ +

Should BE APPENDED to layout first section.

+ +
+ +
+ +

Should REPLACE layout second section.

+ +
+

Extended layout third section.

+

Should BE APPENDED to extended layout third section.

+
+ +
+ +
+ +

EXTENDED LAYOUT, Out of any section, output a deep var Another deep var.

+ +
+ +

+ I am another partial. + This: "!!!" is a specific partial var. + This: "TEST ME!" is a var I share with template. + A shared var skipped: "". +

+ +

FINAL TEMPLATE, Out of any section, ouput a var TEST ME!

+ +
+
+ + + \ No newline at end of file diff --git a/tests/_files/templates/extended.php b/tests/_files/templates/extended.php new file mode 100644 index 0000000..391c8fd --- /dev/null +++ b/tests/_files/templates/extended.php @@ -0,0 +1,33 @@ +layout('layout') ?> + +

EXTENDED LAYOUT, Out of any section, output a deep var v('a.var.in.extended') ?>

+ +section('first') ?> + +

Should BE APPENDED to layout first section.

+ +append() // first ?> + +section('first-child') ?> + +

Should REPLACE layout first-child layout section.

+ +replace() // first-child ?> + +section('second') ?> + +

Should REPLACE layout second section.

+ +
+ + section('third') ?> +

Extended layout third section.

+ append() // third ?> + +
+ +replace(); // second ?> + +
+ buffer() ?> +
diff --git a/tests/_files/templates/final-template.php b/tests/_files/templates/final-template.php new file mode 100644 index 0000000..4a7049d --- /dev/null +++ b/tests/_files/templates/final-template.php @@ -0,0 +1,25 @@ +layout('extended') ?> + +section('a-section') ?> + +

Should BE SUPPLYED by layout.

+ +stop() ?> + + +section('third') ?> + +

Should BE APPENDED to extended layout third section.

+ +append() // third ?> + + +section('another-section') ?> + +

OVERRIDE a section defined in a PARTIAL

+ +replace() // another-section ?> + +insert('partials/partial-2', ['a_partial_var' => '"!!!"'], ['test_me']) ?> + +

FINAL TEMPLATE, Out of any section, ouput a var v('test_me') ?>

diff --git a/tests/_files/templates/layout.php b/tests/_files/templates/layout.php new file mode 100644 index 0000000..7150a08 --- /dev/null +++ b/tests/_files/templates/layout.php @@ -0,0 +1,90 @@ + + + + + <?= $this->v('title', 'Default Title') ?> + + + + + + ww('menu', '
', '
  • %s
  • ') ?> + + wwif('menu', false, '
    %s
    ', '
  • %s
  • ') ?> + +
    + +

    Deep var test: v('a.pretty.deep.var') ?>

    + +

    Default test: v('i_do_not_exist', 'I am a default.') ?>

    + +

    __get test: test_me ?>

    + +

    Raw test: raw('html_content') ?>

    + +

    Autoescape test: html_content ?>

    + +

    Filter test: f('uppercase|reverse', 'lowercase') ?>

    + +

    Advanced test: v('a.var.0|uppercase|reverse') ?>

    + +
    + +
    + returnSomething() ?> +
    + +
    + insert('partials/partial', ['a_partial_var' => '"I am a partial var!"']) ?> +
    + +
    + raw('i_do_not_exist', $this->insert('partials/partial-2', ['a_partial_var' => '"I am a partial var too."'])) ?> +
    + +
    + supply('a-section') ?> +
    + +
    + supply('a-non-existent-section', 'I am here.') ?> +
    + +
    + + section('first') ?> + +

    MAIN LAYOUT, first section

    + +
    + + section('first-child') ?> + +

    MAIN LAYOUT, first child section

    + + stop() // first-child ?> + +
    + +

    MAIN LAYOUT, first section after child

    + + stop() // first ?> + +
    + +
    + + section('second') ?> + +

    MAIN LAYOUT, second section

    + + stop() // second ?> + +
    + +
    + buffer() ?> +
    + + + diff --git a/tests/_files/templates/partials/partial-2.php b/tests/_files/templates/partials/partial-2.php new file mode 100644 index 0000000..6fcd717 --- /dev/null +++ b/tests/_files/templates/partials/partial-2.php @@ -0,0 +1,6 @@ +

    + I am another partial. + This: v('a_partial_var') ?> is a specific partial var. + This: "test_me ?>" is a var I share with template. + A shared var skipped: "v('title') ?>". +

    diff --git a/tests/_files/templates/partials/partial.php b/tests/_files/templates/partials/partial.php new file mode 100644 index 0000000..b788c1e --- /dev/null +++ b/tests/_files/templates/partials/partial.php @@ -0,0 +1,10 @@ +section('another-section') ?> + +

    A SECTION defined in a PARTIAL

    + +stop() ?> + +

    + I am a partial. And this: "test_me ?>" is a var I share with template, but + this v('a_partial_var') ?> is a specific partial var. +

    diff --git a/tests/boot.php b/tests/boot.php new file mode 100644 index 0000000..1edf178 --- /dev/null +++ b/tests/boot.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +$vendor = dirname(dirname(__FILE__)).'/vendor/'; + +if (! realpath($vendor)) { + die('Please install via Composer before running tests.'); +} + +require_once $vendor.'antecedent/patchwork/Patchwork.php'; +require_once $vendor.'autoload.php'; +require_once $vendor.'phpunit/phpunit/src/Framework/Assert/Functions.php'; + +putenv('FOIL_TESTS_BASEPATH='.__DIR__); + +unset($vendor); diff --git a/tests/src/TestCase.php b/tests/src/TestCase.php new file mode 100644 index 0000000..4fd229b --- /dev/null +++ b/tests/src/TestCase.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Foil\Tests; + +use PHPUnit_Framework_TestCase; +use Brain\Monkey; +use Closure; +use InvalidArgumentException; +use LogicException; + +/** + * @author Giuseppe Mazzapica + * @package foil\foil + * @license http://opensource.org/licenses/MIT MIT + */ +class TestCase extends PHPUnit_Framework_TestCase +{ + protected function setUp() + { + parent::setUp(); + Monkey::setUp(); + } + + protected function tearDown() + { + Monkey::tearDown(); + parent::tearDown(); + } + + /** + * @param callable $closure + * @param object $object + * @param array $args + * @return mixed + */ + protected function bindClosure(Closure $closure, $object, array $args = []) + { + /** @var \Closure $closure */ + /** @noinspection PhpUndefinedMethodInspection */ + $closure = Closure::bind($closure, $object, get_class($object)); + + return call_user_func_array($closure, $args); + } + + /** + * @param string $property + * @param $object + * @return mixed + */ + protected function accessPrivateProperty($property, $object) + { + if (! is_string($property) || ! is_object($object)) { + throw new InvalidArgumentException( + __METHOD__.' needs a valid property name and a valid object.' + ); + } + + return $this->bindClosure(function ($property) { + if (! isset($this->$property)) { + throw new LogicException( + "{$property} is not a set on the object." + ); + } + + return $this->$property; + }, $object, [$property]); + } + + /** + * @param string $property + * @param mixed $value + * @param object $object + * @return mixed + */ + protected function setPrivateProperty($property, $value, $object) + { + if (! is_string($property) || ! is_object($object)) { + throw new InvalidArgumentException( + __METHOD__.' needs a valid property name and a valid object.' + ); + } + + return $this->bindClosure(function ($property, $value) { + $this->$property = $value; + }, $object, [$property, $value]); + } +} diff --git a/tests/src/TestCaseFunctional.php b/tests/src/TestCaseFunctional.php new file mode 100644 index 0000000..53e3281 --- /dev/null +++ b/tests/src/TestCaseFunctional.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Foil\Tests; + +use Foil\Foil; + +/** + * @author Giuseppe Mazzapica + * @package foil\foil + * @license http://opensource.org/licenses/MIT MIT + */ +class TestCaseFunctional extends TestCase +{ + /** + * @var \Foil\Engine + */ + protected $engine; + + /** + * @var \Pimple\Container + */ + protected $container; + + /** + * @param array $options + */ + public function initFoil(array $options = []) + { + $base = realpath(getenv('FOIL_TESTS_BASEPATH')).DIRECTORY_SEPARATOR; + $options = array_merge( + [ + 'folders' => [ + 'foo' => $base.implode(DIRECTORY_SEPARATOR, ['_files', 'foo']), + 'bar' => $base.implode(DIRECTORY_SEPARATOR, ['_files', 'bar']), + ], + ], + $options + ); + $app = Foil::boot($options); + $this->container = $this->accessPrivateProperty('container', $app); + $this->engine = $app->engine(); + } +} diff --git a/tests/src/Unit/EngineTest.php b/tests/src/Unit/EngineTest.php new file mode 100644 index 0000000..8cfddd4 --- /dev/null +++ b/tests/src/Unit/EngineTest.php @@ -0,0 +1,361 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foil\Tests\Unit; + +use Foil\Engine; +use Foil\Template\Finder; +use Foil\Tests\TestCase; +use Foil\Kernel\Events; +use Mockery; + +/** + * @author Giuseppe Mazzapica + * @license http://opensource.org/licenses/MIT MIT + * @package Foil + */ +class EngineTest extends TestCase +{ + /** + * @param \Foil\Kernel\Events $events + * @param \Foil\Template\Finder $finder + * @param string $render + * @return \Foil\Engine + */ + private function getEngine(Events $events = null, Finder $finder = null, $render = '') + { + /** @var \Foil\Template\Stack|\Mockery\MockInterface $stack */ + $stack = Mockery::mock('Foil\Template\Stack'); + + /** @var \Foil\Template\Finder|\Mockery\MockInterface $finder */ + $finder = $finder ?: Mockery::mock('Foil\Template\Finder'); + if (is_null($events)) { + /** @var \Foil\Kernel\Events|\Mockery\MockInterface $events */ + $events = Mockery::mock('Foil\Kernel\Events'); + $events + ->shouldReceive('fire') + ->with(Mockery::type('string'), Mockery::type('array')) + ->andReturnNull(); + $events + ->shouldReceive('on') + ->with(Mockery::type('string'), Mockery::type('Closure')) + ->andReturnNull(); + } + + $engine = new Engine($stack, $finder, $events); + + if ($render) { + /** @var \Foil\Template\Stack|\Mockery\MockInterface $template */ + $template = Mockery::mock('Foil\Template\Template'); + $template + ->shouldReceive('render') + ->with(Mockery::type('array')) + ->andReturn($render); + $stack + ->shouldReceive('factory') + ->with(__FILE__, $engine, Mockery::any()) + ->andReturn($template); + + $events + ->shouldReceive('fire') + ->once() + ->with('f.template.render', $template, Mockery::type('array')) + ->andReturnNull(); + $events + ->shouldReceive('fire') + ->once() + ->with('f.template.renderered', $template, $render) + ->andReturnNull(); + } + + return $engine; + } + + /** + * @expectedException \LogicException + */ + public function testCallFailsIfUnsafeFunction() + { + $engine = $this->getEngine(); + $engine->foo(); + } + + public function testCall() + { + /** @var \Foil\Kernel\Events|\Mockery\MockInterface $events */ + $events = Mockery::mock('Foil\Kernel\Events'); + $events + ->shouldReceive('fire') + ->once() + ->with('f.engine.call', 'useData', [['foo' => 'foo']]); + $engine = $this->getEngine($events); + + assertSame($engine, $engine->useData(['foo' => 'foo'])); + } + + public function testFireCallOnEvents() + { + /** @var \Foil\Kernel\Events|\Mockery\MockInterface $events */ + $events = Mockery::mock('Foil\Kernel\Events'); + $events + ->shouldReceive('fire') + ->once() + ->with('foo', 'bar', 'baz'); + $engine = $this->getEngine($events); + $engine->fire('foo', 'bar', 'baz'); + } + + public function testStatusIdleOnStart() + { + $engine = $this->getEngine(); + assertSame(Engine::STATUS_IDLE, $engine->status()); + } + + public function testLoadExtension() + { + /** @var \Foil\Contracts\ExtensionInterface $extension */ + $extension = Mockery::mock('Foil\Contracts\ExtensionInterface'); + /** @var \Foil\Kernel\Events|\Mockery\MockInterface $events */ + $events = Mockery::mock('Foil\Kernel\Events'); + $events + ->shouldReceive('fire') + ->once() + ->with('f.extension.load', $extension, ['foo' => 'foo'], false); + $engine = $this->getEngine($events); + + assertSame($engine, $engine->loadExtension($extension, ['foo' => 'foo'])); + } + + public function testRegisterFilter() + { + $filter = function () { + return true; + }; + /** @var \Foil\Kernel\Events|\Mockery\MockInterface $events */ + $events = Mockery::mock('Foil\Kernel\Events'); + $events + ->shouldReceive('fire') + ->once() + ->with('f.filter.register', 'foo', $filter); + $engine = $this->getEngine($events); + + assertSame($engine, $engine->registerFilter('foo', $filter)); + } + + public function testRegisterFunction() + { + $function = function () { + return true; + }; + /** @var \Foil\Kernel\Events|\Mockery\MockInterface $events */ + $events = Mockery::mock('Foil\Kernel\Events'); + $events + ->shouldReceive('fire') + ->once() + ->with('f.function.register', 'foo', $function, true); + $engine = $this->getEngine($events); + + assertSame($engine, $engine->registerFunction('foo', $function, true)); + } + + public function testRegisterBlock() + { + $block = function () { + return true; + }; + /** @var \Foil\Kernel\Events|\Mockery\MockInterface $events */ + $events = Mockery::mock('Foil\Kernel\Events'); + $events + ->shouldReceive('fire') + ->once() + ->with('f.block.register', 'foo', $block); + $engine = $this->getEngine($events); + + assertSame($engine, $engine->registerBlock('foo', $block)); + } + + public function testSetFolders() + { + /** @var \Foil\Template\Finder|\Mockery\MockInterface $finder */ + $finder = Mockery::mock('Foil\Template\Finder'); + $finder + ->shouldReceive('in') + ->once() + ->with(['foo', 'bar'], true) + ->andReturnNull(); + $engine = $this->getEngine(null, $finder); + + assertSame($engine, $engine->setFolders(['foo', 'bar'])); + } + + public function testAddFolderNoName() + { + /** @var \Foil\Template\Finder|\Mockery\MockInterface $finder */ + $finder = Mockery::mock('Foil\Template\Finder'); + $finder + ->shouldReceive('in') + ->once() + ->with(['foo']) + ->andReturnNull(); + $engine = $this->getEngine(null, $finder); + + assertSame($engine, $engine->addFolder('foo')); + } + + public function testAddFolderName() + { + /** @var \Foil\Template\Finder|\Mockery\MockInterface $finder */ + $finder = Mockery::mock('Foil\Template\Finder'); + $finder + ->shouldReceive('in') + ->once() + ->with(['name' => 'foo']) + ->andReturnNull(); + $engine = $this->getEngine(null, $finder); + + assertSame($engine, $engine->addFolder('foo', 'name')); + } + + public function testFindCallOnFinder() + { + /** @var \Foil\Template\Finder|\Mockery\MockInterface $finder */ + $finder = Mockery::mock('Foil\Template\Finder'); + $finder + ->shouldReceive('find') + ->once() + ->with('foo') + ->andReturn('/foo.php'); + $engine = $this->getEngine(null, $finder); + + assertSame('/foo.php', $engine->find('foo', 'meh')); + } + + public function testRenderFile() + { + $engine = $this->getEngine(null, null, 'Rendered!'); + assertSame('Rendered!', $engine->render(__FILE__)); + } + + public function testRender() + { + /** @var \Foil\Template\Finder|\Mockery\MockInterface $finder */ + $finder = Mockery::mock('Foil\Template\Finder'); + $finder + ->shouldReceive('find') + ->once() + ->with('foo') + ->andReturn(__FILE__); + $engine = $this->getEngine(null, $finder, 'Rendered!'); + assertSame('Rendered!', $engine->render('foo')); + } + + /** + * @expectedException \RuntimeException + */ + public function testRenderFailsIfTemplateNotFound() + { + /** @var \Foil\Template\Finder|\Mockery\MockInterface $finder */ + $finder = Mockery::mock('Foil\Template\Finder'); + $finder + ->shouldReceive('find') + ->once() + ->with('foo') + ->andReturn(false); + $engine = $this->getEngine(null, $finder); + $engine->render('foo'); + } + + public function testRenderTemplate() + { + $engine = $this->getEngine(null, null, 'Rendered!'); + assertSame('Rendered!', $engine->renderTemplate(__FILE__)); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testRenderSectionFailsIfBadSection() + { + $engine = $this->getEngine(); + $engine->renderSection('foo', true); + } + + public function testRenderSectionSingle() + { + /** @var \Foil\Kernel\Events|\Mockery\MockInterface $events */ + $events = Mockery::mock('Foil\Kernel\Events'); + $events + ->shouldReceive('on') + ->once() + ->with('f.sections.content', Mockery::type('Closure')) + ->andReturnNull(); + $events + ->shouldReceive('on') + ->with(Mockery::type('string'), Mockery::type('Closure')) + ->andReturnNull(); + $events + ->shouldReceive('removeListener') + ->once() + ->with('f.sections.content', Mockery::type('Closure')) + ->andReturnNull(); + + $engine = $this->getEngine($events, null, 'foo'); + + assertSame('', $engine->renderSection(__FILE__, 'foo')); + } + + public function testRenderSectionMulti() + { + /** @var \Foil\Kernel\Events|\Mockery\MockInterface $events */ + $events = Mockery::mock('Foil\Kernel\Events'); + $events + ->shouldReceive('on') + ->once() + ->with('f.sections.content', Mockery::type('Closure')) + ->andReturnNull(); + $events + ->shouldReceive('on') + ->with(Mockery::type('string'), Mockery::type('Closure')) + ->andReturnNull(); + $events + ->shouldReceive('removeListener') + ->once() + ->with('f.sections.content', Mockery::type('Closure')) + ->andReturnNull(); + + $engine = $this->getEngine($events, null, 'foo'); + + assertSame(['foo' => '', 'bar' => ''], $engine->renderSection(__FILE__, ['foo', 'bar'])); + } + + public function testRenderSections() + { + /** @var \Foil\Kernel\Events|\Mockery\MockInterface $events */ + $events = Mockery::mock('Foil\Kernel\Events'); + $events + ->shouldReceive('on') + ->once() + ->with('f.sections.content', Mockery::type('Closure')) + ->andReturnNull(); + $events + ->shouldReceive('on') + ->with(Mockery::type('string'), Mockery::type('Closure')) + ->andReturnNull(); + $events + ->shouldReceive('removeListener') + ->once() + ->with('f.sections.content', Mockery::type('Closure')) + ->andReturnNull(); + + $engine = $this->getEngine($events, null, 'foo'); + + assertSame([], $engine->renderSections(__FILE__)); + } +}