mapper = new ComponentHtmlMapper; $this->evaluator = new ExpressionEvaluator; $this->mapper->setComponentMap([ 'Div' => ['tag' => 'div'], 'Li' => ['tag' => 'li'], 'Span' => ['tag' => 'span'], ]); $this->mapper->setTextProps(['text']); $this->mapper->setAllowedAttrs(['class', 'id']); $this->mapper->setAttrMap(['className' => 'class']); } /** * 컴포넌트 배열을 렌더링합니다. * * @param array $components 컴포넌트 정의 배열 * @param array $context 데이터 컨텍스트 * @return string 렌더링된 HTML */ private function render(array $components, array $context = []): string { return $this->mapper->render($components, $context, $this->evaluator); } /** * 항목 변수를 참조하는 if 가 iteration 과 함께 있어도 목록이 통째로 사라지지 않습니다. */ public function test_iteration_with_item_referencing_if_renders_matching_items(): void { $components = [ [ 'name' => 'Li', 'if' => '{{user.is_active}}', 'iteration' => [ 'data' => '{{users}}', 'item_var' => 'user', ], 'props' => ['text' => '{{user.name}}'], ], ]; $context = [ 'users' => [ ['name' => '가', 'is_active' => true], ['name' => '나', 'is_active' => false], ['name' => '다', 'is_active' => true], ], ]; $html = $this->render($components, $context); // 수정 전: 부모 시점에 `user` 가 없어 조건이 거짓 → 빈 문자열(목록 전체 소실) $this->assertStringContainsString('
  • 가
  • ', $html); $this->assertStringContainsString('
  • 다
  • ', $html); $this->assertStringNotContainsString('
  • 나
  • ', $html); } /** * condition 키로 작성해도 같은 순서가 적용됩니다. */ public function test_iteration_with_item_referencing_condition_key(): void { $components = [ [ 'name' => 'Li', 'condition' => '{{row.visible}}', 'iteration' => [ 'source' => '{{rows}}', 'item_var' => 'row', ], 'props' => ['text' => '{{row.label}}'], ], ]; $context = [ 'rows' => [ ['label' => 'A', 'visible' => true], ['label' => 'B', 'visible' => false], ], ]; $html = $this->render($components, $context); $this->assertStringContainsString('
  • A
  • ', $html); $this->assertStringNotContainsString('
  • B
  • ', $html); } /** * 외곽 변수만 참조하는 if 는 종전과 같은 결과를 냅니다 (거짓이면 목록 미렌더). */ public function test_iteration_with_outer_scope_if_false_renders_nothing(): void { $components = [ [ 'name' => 'Li', 'if' => '{{showList}}', 'iteration' => [ 'data' => '{{items}}', 'item_var' => 'item', ], 'props' => ['text' => '{{item.name}}'], ], ]; $context = [ 'showList' => false, 'items' => [['name' => '가'], ['name' => '나']], ]; $this->assertSame('', $this->render($components, $context)); } /** * 외곽 변수만 참조하는 if 가 참이면 전 항목이 렌더됩니다. */ public function test_iteration_with_outer_scope_if_true_renders_all_items(): void { $components = [ [ 'name' => 'Li', 'if' => '{{showList}}', 'iteration' => [ 'data' => '{{items}}', 'item_var' => 'item', ], 'props' => ['text' => '{{item.name}}'], ], ]; $context = [ 'showList' => true, 'items' => [['name' => '가'], ['name' => '나']], ]; $html = $this->render($components, $context); $this->assertStringContainsString('
  • 가
  • ', $html); $this->assertStringContainsString('
  • 나
  • ', $html); } /** * iteration 이 없는 노드의 if 는 종전처럼 즉시 차단합니다 (회귀 방지). */ public function test_if_without_iteration_still_blocks_immediately(): void { $components = [ [ 'name' => 'Div', 'if' => '{{visible}}', 'props' => ['text' => '숨김 대상'], ], ]; $this->assertSame('', $this->render($components, ['visible' => false])); } /** * `{item_var}_index` 자동 변수가 항목 컨텍스트에 주입됩니다. */ public function test_iteration_injects_item_var_index_automatic_variable(): void { $components = [ [ 'name' => 'Li', 'iteration' => [ 'data' => '{{items}}', 'item_var' => 'row', ], 'props' => ['text' => '{{row_index}}'], ], ]; $context = ['items' => [['name' => '가'], ['name' => '나'], ['name' => '다']]]; $html = $this->render($components, $context); $this->assertStringContainsString('
  • 0
  • ', $html); $this->assertStringContainsString('
  • 1
  • ', $html); $this->assertStringContainsString('
  • 2
  • ', $html); } /** * `{item_var}_index` 주입이 명시적 index_var 를 덮어쓰지 않습니다. */ public function test_item_var_index_coexists_with_explicit_index_var(): void { $components = [ [ 'name' => 'Li', 'iteration' => [ 'data' => '{{items}}', 'item_var' => 'row', 'index_var' => 'i', ], 'children' => [ ['name' => 'Span', 'props' => ['text' => '{{i}}']], ['name' => 'Span', 'props' => ['text' => '{{row_index}}']], ], ], ]; $context = ['items' => [['name' => '가'], ['name' => '나']]]; $html = $this->render($components, $context); $this->assertStringContainsString('
  • 00
  • ', $html); $this->assertStringContainsString('
  • 11
  • ', $html); } /** * 항목 조건이 자동 변수(`{item_var}_index`)를 참조해도 항목별로 평가됩니다. */ public function test_item_condition_can_reference_injected_index_variable(): void { $components = [ [ 'name' => 'Li', 'if' => '{{row_index < 2}}', 'iteration' => [ 'data' => '{{items}}', 'item_var' => 'row', ], 'props' => ['text' => '{{row.name}}'], ], ]; $context = ['items' => [['name' => '가'], ['name' => '나'], ['name' => '다']]]; $html = $this->render($components, $context); $this->assertStringContainsString('
  • 가
  • ', $html); $this->assertStringContainsString('
  • 나
  • ', $html); $this->assertStringNotContainsString('
  • 다
  • ', $html); } }