Skip to content

Commit a0d6ab7

Browse files
committed
test: Test logic executor and surrounding functionality
1 parent a24d281 commit a0d6ab7

4 files changed

Lines changed: 326 additions & 0 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
namespace GT\WebEngine\Test\Fixture;
3+
4+
use Attribute;
5+
6+
#[Attribute(Attribute::TARGET_FUNCTION)]
7+
class TestAttribute {
8+
public function __construct(
9+
public readonly string $name,
10+
public readonly int $count,
11+
) {}
12+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
namespace GT\WebEngine\Test\Fixture;
3+
4+
use Gt\Logger\LogHandler\LogHandler;
5+
6+
class TestLogHandler extends LogHandler {
7+
/** @var array<int, array{level:string,message:string,context:array<string, mixed>}> */
8+
public static array $records = [];
9+
10+
public function handle(
11+
string $level,
12+
string $message,
13+
array $context = []
14+
):void {
15+
self::$records[] = [
16+
"level" => $level,
17+
"message" => $message,
18+
"context" => $context,
19+
];
20+
}
21+
22+
protected function unwrapContext(array $context):string {
23+
return json_encode($context) ?: "";
24+
}
25+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
namespace GT\WebEngine\Test\Logic;
3+
4+
use GT\WebEngine\Logic\HTMLDocumentProcessor;
5+
use Gt\Dom\Element;
6+
use Gt\Dom\HTMLDocument;
7+
use Gt\Routing\Assembly;
8+
use Gt\Routing\Path\DynamicPath;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class HTMLDocumentProcessorTest extends TestCase {
12+
private string $tmpDir;
13+
private string $componentDir;
14+
private string $partialDir;
15+
16+
protected function setUp():void {
17+
parent::setUp();
18+
$this->tmpDir = getcwd() . "/test/phpunit/tmplogichtml" . uniqid();
19+
$this->componentDir = $this->tmpDir . "/components";
20+
$this->partialDir = $this->tmpDir . "/partials";
21+
mkdir($this->componentDir, recursive: true);
22+
mkdir($this->partialDir, recursive: true);
23+
}
24+
25+
protected function tearDown():void {
26+
$this->deleteDir($this->tmpDir);
27+
parent::tearDown();
28+
}
29+
30+
public function testProcessDynamicPath_addsUriAndDirectoryClasses():void {
31+
$viewAssembly = new Assembly();
32+
$viewAssembly->add("page/blog/@id/article.html");
33+
$processor = new HTMLDocumentProcessor("components", "partials");
34+
$document = new HTMLDocument("<!doctype html><body></body>");
35+
36+
$processor->processDynamicPath(
37+
$document,
38+
new DynamicPath("/blog/42/article/", $viewAssembly),
39+
);
40+
41+
$classes = iterator_to_array($document->body->classList);
42+
self::assertContains("uri--blog--_id--article", $classes);
43+
self::assertContains("dir--blog", $classes);
44+
self::assertContains("dir--blog--_id", $classes);
45+
self::assertContains("dir--blog--_id--article", $classes);
46+
}
47+
48+
public function testProcessPartialContent_expandsComponentsAndPartialsAndRegistersOnlyLogicBackedComponents():void {
49+
file_put_contents(
50+
$this->componentDir . "/site-card.html",
51+
"<article><form method='post'><button>Save</button></form></article>",
52+
);
53+
file_put_contents($this->componentDir . "/site-card.php", "<?php\n");
54+
file_put_contents($this->componentDir . "/no-logic.html", "<aside>No logic</aside>");
55+
file_put_contents(
56+
$this->partialDir . "/layout.html",
57+
"<!doctype html><html><body><section data-partial></section></body></html>",
58+
);
59+
60+
$document = new HTMLDocument(
61+
"<!doctype html><!-- extends = layout --><site-card></site-card><no-logic></no-logic><main>Page</main>"
62+
);
63+
$processor = new HTMLDocumentProcessor(
64+
ltrim(substr($this->componentDir, strlen(getcwd())), "/"),
65+
ltrim(substr($this->partialDir, strlen(getcwd())), "/"),
66+
);
67+
68+
$componentList = $processor->processPartialContent($document);
69+
70+
self::assertCount(1, $componentList);
71+
$component = $componentList[0];
72+
$component->assembly->rewind();
73+
self::assertSame(
74+
ltrim(substr($this->componentDir, strlen(getcwd())), "/") . "/site-card.php",
75+
$component->assembly->current(),
76+
);
77+
self::assertInstanceOf(Element::class, $component->component);
78+
self::assertSame("site-card", strtolower($component->component->tagName));
79+
self::assertSame("site-card", $document->querySelector("input[name='__component']")?->getAttribute("value"));
80+
self::assertSame("section", strtolower($document->body->firstElementChild->tagName));
81+
self::assertStringContainsString("<main>Page</main>", (string)$document);
82+
self::assertStringContainsString("<aside>No logic</aside>", (string)$document);
83+
}
84+
85+
public function testProcessPartialContent_ignoresMissingDirectories():void {
86+
$document = new HTMLDocument("<!doctype html><body><site-card></site-card></body>");
87+
$processor = new HTMLDocumentProcessor(
88+
"test/phpunit/does-not-exist-components",
89+
"test/phpunit/does-not-exist-partials",
90+
);
91+
92+
$componentList = $processor->processPartialContent($document);
93+
94+
self::assertCount(0, $componentList);
95+
self::assertSame("<site-card></site-card>", trim($document->body->innerHTML));
96+
}
97+
98+
private function deleteDir(string $dir):void {
99+
if(!is_dir($dir)) {
100+
return;
101+
}
102+
103+
foreach(scandir($dir) ?: [] as $item) {
104+
if($item === "." || $item === "..") {
105+
continue;
106+
}
107+
108+
$path = $dir . "/" . $item;
109+
if(is_dir($path)) {
110+
$this->deleteDir($path);
111+
}
112+
else {
113+
unlink($path);
114+
}
115+
}
116+
117+
rmdir($dir);
118+
}
119+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
namespace GT\WebEngine\Test\Logic;
3+
4+
require_once __DIR__ . "/../Fixture/TestAttribute.php";
5+
6+
use GT\WebEngine\Logic\LogicExecutor;
7+
use GT\WebEngine\Logic\LogicProjectNamespace;
8+
use GT\WebEngine\Logic\LogicStreamHandler;
9+
use Gt\Routing\Assembly;
10+
use Gt\ServiceContainer\Injector;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class LogicExecutorTest extends TestCase {
14+
private string $tmpDir;
15+
16+
protected function setUp():void {
17+
parent::setUp();
18+
$this->tmpDir = getcwd() . "/test/phpunit/tmplogic" . uniqid();
19+
mkdir($this->tmpDir, recursive: true);
20+
new LogicStreamHandler()->setup();
21+
}
22+
23+
protected function tearDown():void {
24+
$this->deleteDir($this->tmpDir);
25+
parent::tearDown();
26+
}
27+
28+
public function testInvoke_executesClassMethodFromProjectNamespace():void {
29+
$relativePath = $this->createLogicFile("ClassPage.php", "<?php\n");
30+
$projectClassName = (string)(new LogicProjectNamespace($relativePath, "Example\\App"));
31+
[$projectNamespace, $className] = $this->splitClassName($projectClassName);
32+
file_put_contents(
33+
getcwd() . "/" . $relativePath,
34+
"<?php\nnamespace $projectNamespace;\nclass $className { public function go():void {} }\n",
35+
);
36+
37+
$assembly = new Assembly();
38+
$assembly->add($relativePath);
39+
$extraArgs = ["flag" => "value"];
40+
41+
$injector = $this->createMock(Injector::class);
42+
$injector->expects(self::once())
43+
->method("invoke")
44+
->with(
45+
self::callback(fn(object $instance):bool => $instance::class === $projectClassName),
46+
"go",
47+
$extraArgs,
48+
);
49+
50+
$sut = new LogicExecutor("Example\\App", $injector);
51+
$invoked = iterator_to_array($sut->invoke($assembly, "go", $extraArgs), false);
52+
53+
self::assertSame(["$relativePath::go()"], $invoked);
54+
}
55+
56+
public function testInvoke_executesProjectNamespacedFunctionWhenNoClassExists():void {
57+
$relativePath = $this->createLogicFile("FunctionPage.php", "<?php\n");
58+
$projectNamespace = (string)(new LogicProjectNamespace($relativePath, "Example\\App"));
59+
file_put_contents(
60+
getcwd() . "/" . $relativePath,
61+
"<?php\nnamespace $projectNamespace;\nfunction go():void {}\n",
62+
);
63+
64+
$assembly = new Assembly();
65+
$assembly->add($relativePath);
66+
$extraArgs = ["token" => "abc"];
67+
$expectedFunction = (string)(new LogicProjectNamespace($relativePath, "Example\\App")) . "\\go";
68+
69+
$injector = $this->createMock(Injector::class);
70+
$injector->expects(self::once())
71+
->method("invoke")
72+
->with(null, $expectedFunction, $extraArgs);
73+
74+
$sut = new LogicExecutor("Example\\App", $injector);
75+
$invoked = iterator_to_array($sut->invoke($assembly, "go", $extraArgs), false);
76+
77+
self::assertSame(["$relativePath::go()"], $invoked);
78+
}
79+
80+
public function testInvoke_executesStreamWrappedFunctionAndIncludesAttributeMetadata():void {
81+
$relativePath = $this->createLogicFile(
82+
"attribute-demo.php",
83+
<<<'PHP'
84+
<?php
85+
use GT\WebEngine\Test\Fixture\TestAttribute;
86+
87+
#[TestAttribute("alpha", 123)]
88+
function go():void {}
89+
PHP
90+
);
91+
92+
$assembly = new Assembly();
93+
$assembly->add($relativePath);
94+
$extraArgs = ["payload" => 123];
95+
96+
$wrappedNamespace = "GT\\AppLogic\\" . str_replace(["/", ".", "-", "@", "+", "~", "%"], ["\\", "_", "_", "_", "_", "_", "_"], $relativePath);
97+
$expectedFunction = $wrappedNamespace . "\\go";
98+
99+
$injector = $this->createMock(Injector::class);
100+
$injector->expects(self::once())
101+
->method("invoke")
102+
->with(null, $expectedFunction, $extraArgs);
103+
104+
$sut = new LogicExecutor("Example\\App", $injector);
105+
$invoked = iterator_to_array($sut->invoke($assembly, "go", $extraArgs), false);
106+
107+
self::assertSame([
108+
$relativePath . '::go()#GT\WebEngine\Test\Fixture\TestAttribute("alpha",123)',
109+
], $invoked);
110+
}
111+
112+
public function testInvoke_skipsMissingHandlersWithoutCallingInjector():void {
113+
$relativePath = $this->createLogicFile(
114+
"missing.php",
115+
<<<'PHP'
116+
<?php
117+
function different_function():void {}
118+
PHP
119+
);
120+
121+
$assembly = new Assembly();
122+
$assembly->add($relativePath);
123+
124+
$injector = $this->createMock(Injector::class);
125+
$injector->expects(self::never())
126+
->method("invoke");
127+
128+
$sut = new LogicExecutor("Example\\App", $injector);
129+
$invoked = iterator_to_array($sut->invoke($assembly, "go"), false);
130+
131+
self::assertSame([], $invoked);
132+
}
133+
134+
private function createLogicFile(string $name, string $contents):string {
135+
$file = $this->tmpDir . "/" . $name;
136+
file_put_contents($file, $contents);
137+
return ltrim(substr($file, strlen(getcwd())), "/");
138+
}
139+
140+
private function deleteDir(string $dir):void {
141+
if(!is_dir($dir)) {
142+
return;
143+
}
144+
145+
foreach(scandir($dir) ?: [] as $item) {
146+
if($item === "." || $item === "..") {
147+
continue;
148+
}
149+
150+
$path = $dir . "/" . $item;
151+
if(is_dir($path)) {
152+
$this->deleteDir($path);
153+
}
154+
else {
155+
unlink($path);
156+
}
157+
}
158+
159+
rmdir($dir);
160+
}
161+
162+
/** @return array{0:string,1:string} */
163+
private function splitClassName(string $fqcn):array {
164+
$pos = strrpos($fqcn, "\\");
165+
return [
166+
substr($fqcn, 0, $pos),
167+
substr($fqcn, $pos + 1),
168+
];
169+
}
170+
}

0 commit comments

Comments
 (0)