1
0

TwigViewTest.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Http;
  4. use App\Http\View;
  5. use App\Tests\TestCase;
  6. use ReflectionClass;
  7. use ReflectionProperty;
  8. /**
  9. * Smoke tests for the Twig-backed View. Phase 19 swapped the bare-PHP renderer
  10. * for Twig 3 — these confirm that the controllers' (name, $data) call shape
  11. * still produces HTML containing the markers each page is expected to carry.
  12. */
  13. final class TwigViewTest extends TestCase
  14. {
  15. private View $view;
  16. protected function setUp(): void
  17. {
  18. parent::setUp();
  19. $this->view = new View(__DIR__ . '/../../views');
  20. }
  21. private function makeUser(bool $isAdmin = true): object
  22. {
  23. $u = new \stdClass();
  24. $u->id = 1;
  25. $u->email = 'alice@example.com';
  26. $u->displayName = 'Alice';
  27. $u->isAdmin = $isAdmin;
  28. return $u;
  29. }
  30. public function testHomeRendersForSignedInAdmin(): void
  31. {
  32. $html = $this->view->render('home', [
  33. 'title' => 'Sprint Planner',
  34. 'csrfToken' => 'tok',
  35. 'currentUser' => $this->makeUser(true),
  36. 'schemaVersion' => 3,
  37. 'dbPath' => '/tmp/x',
  38. 'appEnv' => 'production',
  39. 'oidcConfigured' => false,
  40. 'localAdminEnabled' => true,
  41. 'authError' => false,
  42. 'sprintRows' => [],
  43. ]);
  44. self::assertStringContainsString('<title>Sprint Planner</title>', $html);
  45. self::assertStringContainsString('Sprints', $html);
  46. self::assertStringContainsString('Alice', $html); // displayName
  47. self::assertStringContainsString('admin', $html); // admin badge
  48. self::assertStringContainsString('/auth/logout', $html);
  49. // Phase 19: vendored Alpine + htmx + sortable scripts must be linked.
  50. self::assertStringContainsString('/assets/js/vendor/alpine-csp.min.js', $html);
  51. self::assertStringContainsString('/assets/js/vendor/htmx.min.js', $html);
  52. }
  53. public function testHomeForAnonymousUserHidesRuntimePanel(): void
  54. {
  55. $html = $this->view->render('home', [
  56. 'title' => 'Sprint Planner',
  57. 'csrfToken' => 'tok',
  58. 'currentUser' => null,
  59. 'schemaVersion' => 3,
  60. 'dbPath' => '/var/data/app.sqlite',
  61. 'appEnv' => 'production',
  62. 'oidcConfigured' => true,
  63. 'localAdminEnabled' => true,
  64. 'authError' => false,
  65. 'sprintRows' => [],
  66. ]);
  67. self::assertStringContainsString('Sign in with Microsoft', $html);
  68. // R01-N02: the Runtime <details> panel must not leak PHP_VERSION,
  69. // dbPath, schema version, OIDC/local-admin flags to anonymous visitors.
  70. self::assertStringNotContainsString('Runtime', $html);
  71. self::assertStringNotContainsString('Schema version', $html);
  72. self::assertStringNotContainsString('/var/data/app.sqlite', $html);
  73. self::assertStringNotContainsString(PHP_VERSION, $html);
  74. // R01-N31 falls out of the same gate: no /healthz hint either.
  75. self::assertStringNotContainsString('/healthz', $html);
  76. }
  77. public function testAuditRendersWithEmptyResults(): void
  78. {
  79. $html = $this->view->render('audit/index', [
  80. 'title' => 'Audit',
  81. 'csrfToken' => 'tok',
  82. 'currentUser' => $this->makeUser(true),
  83. 'filters' => [
  84. 'user_email' => '',
  85. 'action' => '',
  86. 'entity_type' => '',
  87. 'entity_id' => '',
  88. 'from_date' => '',
  89. 'to_date' => '',
  90. ],
  91. 'page' => 1,
  92. 'pages' => 1,
  93. 'pageSize' => 50,
  94. 'total' => 0,
  95. 'rows' => [],
  96. 'actions' => ['CREATE', 'UPDATE', 'DELETE'],
  97. 'entityTypes' => ['worker', 'sprint'],
  98. 'users' => [],
  99. ]);
  100. self::assertStringContainsString('Audit log', $html);
  101. self::assertStringContainsString('No audit rows match', $html);
  102. // hx-boost wired on the filter form (Phase 19).
  103. self::assertStringContainsString('hx-boost="true"', $html);
  104. }
  105. public function testSprintsShowRendersTaskGridAndStatusFilter(): void
  106. {
  107. $sprint = (new ReflectionClass(\App\Domain\Sprint::class))->newInstanceWithoutConstructor();
  108. foreach ([
  109. 'id' => 1, 'name' => 'S1',
  110. 'startDate' => '2026-01-01', 'endDate' => '2026-01-15',
  111. 'reserveFraction' => 0.2, 'isArchived' => false, 'createdAt' => '2026-01-01',
  112. ] as $k => $v) {
  113. (new ReflectionProperty($sprint, $k))->setValue($sprint, $v);
  114. }
  115. $weekClass = new ReflectionClass(\App\Domain\SprintWeek::class);
  116. $w = $weekClass->newInstanceWithoutConstructor();
  117. foreach ([
  118. 'id' => 10, 'sprintId' => 1, 'isoWeek' => 1,
  119. 'startDate' => '2026-01-05', 'sortOrder' => 1,
  120. 'maxWorkingDays' => 5, 'activeDaysMask' => 31,
  121. ] as $k => $v) {
  122. (new ReflectionProperty($w, $k))->setValue($w, $v);
  123. }
  124. $swClass = new ReflectionClass(\App\Domain\SprintWorker::class);
  125. $sw = $swClass->newInstanceWithoutConstructor();
  126. foreach ([
  127. 'id' => 100, 'sprintId' => 1, 'workerId' => 50, 'workerName' => 'Bob',
  128. 'sortOrder' => 1, 'rtb' => 0.1,
  129. ] as $k => $v) {
  130. (new ReflectionProperty($sw, $k))->setValue($sw, $v);
  131. }
  132. $html = $this->view->render('sprints/show', [
  133. 'title' => 'S1',
  134. 'csrfToken' => 'tok',
  135. 'currentUser' => $this->makeUser(true),
  136. 'sprint' => $sprint,
  137. 'weeks' => [$w],
  138. 'sprintWorkers' => [$sw],
  139. 'grid' => [100 => [10 => 2.5]],
  140. 'capacity' => [100 => [
  141. 'ressourcen' => 2.5,
  142. 'after_reserves' => 2.0,
  143. 'committed_prio1' => 0.0,
  144. 'available' => 2.0,
  145. ]],
  146. 'tasks' => [],
  147. 'taskGrid' => [],
  148. 'statusGrid' => [],
  149. 'ownerChoices' => [],
  150. 'taskStatusEnabled' => true,
  151. ]);
  152. self::assertStringContainsString('data-sprint-root', $html);
  153. self::assertStringContainsString('data-sprint-id="1"', $html);
  154. self::assertStringContainsString('data-task-status-enabled="1"', $html);
  155. self::assertStringContainsString('Status', $html); // status filter visible
  156. self::assertStringContainsString('No tasks yet', $html);
  157. // Phase 19: page-specific JS still loaded.
  158. self::assertStringContainsString('/assets/js/sprint-planner.js', $html);
  159. }
  160. }