From f2b6196565cbd3b94884078f2994e4ab5f4f29bb Mon Sep 17 00:00:00 2001 From: Brian Love Date: Thu, 30 Apr 2026 17:24:12 -0700 Subject: [PATCH 1/4] docs(superpowers): plan for genui phase 2 test gap closure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds unit-test specs for ContainerComponent and DashboardGridComponent — the two views shipped in PR #127 (9b621521) without specs. Co-Authored-By: Claude Opus 4 --- .../2026-04-30-genui-phase2-test-gaps.md | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-30-genui-phase2-test-gaps.md diff --git a/docs/superpowers/plans/2026-04-30-genui-phase2-test-gaps.md b/docs/superpowers/plans/2026-04-30-genui-phase2-test-gaps.md new file mode 100644 index 000000000..edf217a25 --- /dev/null +++ b/docs/superpowers/plans/2026-04-30-genui-phase2-test-gaps.md @@ -0,0 +1,289 @@ +# Generative UI Phase 2 — Test Gap Closure Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add unit-test coverage for the two `cockpit/chat/generative-ui/angular/` view components shipped in PR #127 (`9b621521`) without specs — `ContainerComponent` and `DashboardGridComponent` — so all six dashboard views meet the example's testing convention. + +**Architecture:** Two new `*.spec.ts` files following the existing pattern in `views/stat-card.component.spec.ts` and `views/bar-chart.component.spec.ts`: `TestBed`-instantiated standalone component + `componentRef.setInput()` for input wiring + DOM assertions. No backend, no mocks beyond what `RenderElementComponent` requires; both target components have minimal logic. + +**Tech Stack:** Angular 21 standalone components, `@angular/build:unit-test` runner (already configured at `cockpit/chat/generative-ui/angular/project.json`), `@angular/core/testing` `TestBed`/`ComponentFixture`. + +**Out of scope (deliberately deferred):** +- E2E "send 'show me the dashboard' → assert stat-card visible" — needs live LangGraph backend; warrants its own plan with CI plumbing. +- Landing the stranded `backup/genui-phase2-docs` design+plan — superseded by post-refactor specs (`2026-04-21-chat-runtime-decoupling-design`, `2026-04-25-events-on-agent-contract-design`); branch should be deleted after this PR merges. +- Adding skeleton/loading state to `ContainerComponent` or `DashboardGridComponent` — neither component owns rendered data; skeleton state is the responsibility of leaf views (`stat_card`, `line_chart`, `bar_chart`, `data_grid`), which are already tested. + +--- + +## File Structure + +### New files +- `cockpit/chat/generative-ui/angular/src/app/views/container.component.spec.ts` +- `cockpit/chat/generative-ui/angular/src/app/views/dashboard-grid.component.spec.ts` + +### Modified files +- None. + +### Deleted files +- None. (Backup branch `backup/genui-phase2-docs` is git-administrative cleanup, handled outside this plan.) + +--- + +## Reference Implementations Under Test + +For context while writing the specs (do not modify these files): + +`cockpit/chat/generative-ui/angular/src/app/views/container.component.ts`: +```ts +// SPDX-License-Identifier: MIT +import { Component, computed, input } from '@angular/core'; +import type { Spec } from '@json-render/core'; +import { RenderElementComponent } from '@ngaf/render'; + +@Component({ + selector: 'app-container', + standalone: true, + imports: [RenderElementComponent], + template: ` +
+ @for (key of childKeys(); track key) { + + } +
+ `, +}) +export class ContainerComponent { + readonly childKeys = input([]); + readonly spec = input.required(); + readonly direction = input<'row' | 'column'>('column'); + + readonly layoutClass = computed(() => + this.direction() === 'row' + ? 'flex flex-row flex-wrap gap-3' + : 'flex flex-col gap-3' + ); +} +``` + +`cockpit/chat/generative-ui/angular/src/app/views/dashboard-grid.component.ts`: +```ts +// SPDX-License-Identifier: MIT +import { Component, input } from '@angular/core'; +import type { Spec } from '@json-render/core'; +import { RenderElementComponent } from '@ngaf/render'; + +@Component({ + selector: 'app-dashboard-grid', + standalone: true, + imports: [RenderElementComponent], + template: ` +
+ @for (key of childKeys(); track key) { + + } +
+ `, +}) +export class DashboardGridComponent { + readonly childKeys = input([]); + readonly spec = input.required(); +} +``` + +Both components delegate child rendering to `` and expose only structural inputs. Tests therefore assert: +1. The wrapping `
` carries the correct layout class. +2. The expected number of `` children are emitted (one per `childKeys` entry). +3. `ContainerComponent` switches the layout class when `direction` toggles. + +--- + +## Task 1: Add `ContainerComponent` spec + +**Files:** +- Create: `cockpit/chat/generative-ui/angular/src/app/views/container.component.spec.ts` + +- [ ] **Step 1: Write the failing test file** + +Create `cockpit/chat/generative-ui/angular/src/app/views/container.component.spec.ts`: + +```ts +// SPDX-License-Identifier: MIT +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import type { Spec } from '@json-render/core'; +import { ContainerComponent } from './container.component'; + +describe('ContainerComponent', () => { + let fixture: ComponentFixture; + + // Minimal Spec satisfying the input.required() — children resolution + // is delegated to , so the spec body itself is not exercised. + const emptySpec: Spec = { elements: {}, root: 'root' }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ContainerComponent], + }).compileComponents(); + fixture = TestBed.createComponent(ContainerComponent); + }); + + it('applies column flex classes by default', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.detectChanges(); + const wrapper = fixture.nativeElement.querySelector('div'); + expect(wrapper?.className).toContain('flex-col'); + expect(wrapper?.className).not.toContain('flex-row'); + }); + + it('applies row flex classes when direction is "row"', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.componentRef.setInput('direction', 'row'); + fixture.detectChanges(); + const wrapper = fixture.nativeElement.querySelector('div'); + expect(wrapper?.className).toContain('flex-row'); + expect(wrapper?.className).not.toContain('flex-col'); + }); + + it('renders one render-element per childKey', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.componentRef.setInput('childKeys', ['a', 'b', 'c']); + fixture.detectChanges(); + const elements = fixture.nativeElement.querySelectorAll('render-element'); + expect(elements.length).toBe(3); + }); + + it('renders no render-element children when childKeys is empty', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.componentRef.setInput('childKeys', []); + fixture.detectChanges(); + const elements = fixture.nativeElement.querySelectorAll('render-element'); + expect(elements.length).toBe(0); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it passes** + +Run: `npx nx test cockpit-chat-generative-ui-angular --test-path-pattern=container.component.spec` +Expected: 4 tests pass. + +If the test runner doesn't filter by path pattern, run the full project test target: +Run: `npx nx test cockpit-chat-generative-ui-angular` +Expected: all existing specs pass + 4 new ContainerComponent tests pass. + +- [ ] **Step 3: Commit** + +```bash +git add cockpit/chat/generative-ui/angular/src/app/views/container.component.spec.ts +git commit -m "test(cockpit): cover ContainerComponent layout and child rendering" +``` + +--- + +## Task 2: Add `DashboardGridComponent` spec + +**Files:** +- Create: `cockpit/chat/generative-ui/angular/src/app/views/dashboard-grid.component.spec.ts` + +- [ ] **Step 1: Write the failing test file** + +Create `cockpit/chat/generative-ui/angular/src/app/views/dashboard-grid.component.spec.ts`: + +```ts +// SPDX-License-Identifier: MIT +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import type { Spec } from '@json-render/core'; +import { DashboardGridComponent } from './dashboard-grid.component'; + +describe('DashboardGridComponent', () => { + let fixture: ComponentFixture; + + const emptySpec: Spec = { elements: {}, root: 'root' }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DashboardGridComponent], + }).compileComponents(); + fixture = TestBed.createComponent(DashboardGridComponent); + }); + + it('applies vertical flex layout with section spacing', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.detectChanges(); + const wrapper = fixture.nativeElement.querySelector('div'); + expect(wrapper?.className).toContain('flex-col'); + expect(wrapper?.className).toContain('gap-6'); + }); + + it('renders one render-element per childKey', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.componentRef.setInput('childKeys', ['stats_row', 'charts_row', 'table_section']); + fixture.detectChanges(); + const elements = fixture.nativeElement.querySelectorAll('render-element'); + expect(elements.length).toBe(3); + }); + + it('renders no render-element children when childKeys is empty', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.componentRef.setInput('childKeys', []); + fixture.detectChanges(); + const elements = fixture.nativeElement.querySelectorAll('render-element'); + expect(elements.length).toBe(0); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it passes** + +Run: `npx nx test cockpit-chat-generative-ui-angular` +Expected: full project test target green, including the 3 new DashboardGridComponent tests. + +- [ ] **Step 3: Commit** + +```bash +git add cockpit/chat/generative-ui/angular/src/app/views/dashboard-grid.component.spec.ts +git commit -m "test(cockpit): cover DashboardGridComponent layout and child rendering" +``` + +--- + +## Task 3: Verify whole-project test suite + lint + +**Files:** none (verification only) + +- [ ] **Step 1: Run the full unit test suite for the example app** + +Run: `npx nx test cockpit-chat-generative-ui-angular` +Expected: every spec under `cockpit/chat/generative-ui/angular/src/app/views/` passes — 4 existing (`stat-card`, `line-chart`, `bar-chart`, `data-grid`) + 2 new (`container`, `dashboard-grid`). + +- [ ] **Step 2: Run lint on touched files** + +Run: `npx nx lint cockpit-chat-generative-ui-angular` +Expected: no errors. If lint is not configured for this project, skip. + +- [ ] **Step 3 (optional): Run the broader test sweep used by CI** + +Run: `npx nx affected -t test --base=origin/main` +Expected: passes — new specs are leaf-only and shouldn't ripple. + +If green, the branch is ready to push and PR. + +--- + +## Self-Review Checklist + +Run before opening the PR: + +- [ ] Both new spec files exist at the paths above and import `Spec` from `@json-render/core`. +- [ ] Test names describe behavior (layout class, child count), not implementation. +- [ ] No `RenderElementComponent` is imported into the spec — assertions inspect the rendered `` selector strings, not internals. +- [ ] No mocks: `TestBed.configureTestingModule({ imports: [Component] })` is the entire setup, matching `stat-card.component.spec.ts`. +- [ ] License header `// SPDX-License-Identifier: MIT` is present on both files. +- [ ] `npx nx test cockpit-chat-generative-ui-angular` passes with all view specs green. + +--- + +## Execution Handoff + +Branch: `test/genui-phase2-view-coverage` (cut from `main`). + +Recommended path: subagent-driven-development — two leaf tasks + one verification task is well-scoped for fresh-context implementer dispatches with two-stage review per task. From ed60a98cd078a24a3e1b5b6fd18d34f07bf62936 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Thu, 30 Apr 2026 17:28:56 -0700 Subject: [PATCH 2/4] docs(superpowers): fix genui test plan to stub The original spec code imported ContainerComponent / DashboardGridComponent directly into TestBed without addressing that is itself a standalone component which calls `inject(RENDER_CONTEXT)`. With non-empty childKeys, Angular instantiated the real RenderElementComponent and threw NG0201. Plan now uses TestBed.overrideComponent to swap in a local StubRenderElementComponent that matches the selector and public inputs. Co-Authored-By: Claude Opus 4 --- .../2026-04-30-genui-phase2-test-gaps.md | 55 ++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/docs/superpowers/plans/2026-04-30-genui-phase2-test-gaps.md b/docs/superpowers/plans/2026-04-30-genui-phase2-test-gaps.md index edf217a25..ecf84ecc3 100644 --- a/docs/superpowers/plans/2026-04-30-genui-phase2-test-gaps.md +++ b/docs/superpowers/plans/2026-04-30-genui-phase2-test-gaps.md @@ -4,7 +4,7 @@ **Goal:** Add unit-test coverage for the two `cockpit/chat/generative-ui/angular/` view components shipped in PR #127 (`9b621521`) without specs — `ContainerComponent` and `DashboardGridComponent` — so all six dashboard views meet the example's testing convention. -**Architecture:** Two new `*.spec.ts` files following the existing pattern in `views/stat-card.component.spec.ts` and `views/bar-chart.component.spec.ts`: `TestBed`-instantiated standalone component + `componentRef.setInput()` for input wiring + DOM assertions. No backend, no mocks beyond what `RenderElementComponent` requires; both target components have minimal logic. +**Architecture:** Two new `*.spec.ts` files following the existing pattern in `views/stat-card.component.spec.ts` and `views/bar-chart.component.spec.ts`: `TestBed`-instantiated standalone component + `componentRef.setInput()` for input wiring + DOM assertions. Both target components import `RenderElementComponent`, which calls `inject(RENDER_CONTEXT)` and would throw NG0201 if instantiated by TestBed; the specs use `TestBed.overrideComponent(...).remove/add({ imports })` to swap in a local `StubRenderElementComponent` that matches ``'s selector and public inputs. No backend, no service mocks. **Tech Stack:** Angular 21 standalone components, `@angular/build:unit-test` runner (already configured at `cockpit/chat/generative-ui/angular/project.json`), `@angular/core/testing` `TestBed`/`ComponentFixture`. @@ -108,10 +108,25 @@ Create `cockpit/chat/generative-ui/angular/src/app/views/container.component.spe ```ts // SPDX-License-Identifier: MIT -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, input } from '@angular/core'; import type { Spec } from '@json-render/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RenderElementComponent } from '@ngaf/render'; import { ContainerComponent } from './container.component'; +// Stub matching 's selector + public inputs. Swapped into +// ContainerComponent's imports via overrideComponent so Angular doesn't +// instantiate the real RenderElementComponent (which requires RENDER_CONTEXT). +@Component({ + selector: 'render-element', + standalone: true, + template: '', +}) +class StubRenderElementComponent { + readonly elementKey = input(''); + readonly spec = input(undefined); +} + describe('ContainerComponent', () => { let fixture: ComponentFixture; @@ -122,7 +137,12 @@ describe('ContainerComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ContainerComponent], - }).compileComponents(); + }) + .overrideComponent(ContainerComponent, { + remove: { imports: [RenderElementComponent] }, + add: { imports: [StubRenderElementComponent] }, + }) + .compileComponents(); fixture = TestBed.createComponent(ContainerComponent); }); @@ -161,6 +181,8 @@ describe('ContainerComponent', () => { }); ``` +> **Why the stub:** `RenderElementComponent` is a standalone component imported by `ContainerComponent`. When Angular sees `` in the rendered template, it instantiates the real component, which calls `inject(RENDER_CONTEXT)` — a non-optional injection that throws `NG0201` in a TestBed without the full render pipeline wired up. `TestBed.overrideComponent(...).remove/add({ imports })` swaps in a stub matching the selector + inputs so DOM assertions on `` count still work, but nothing tries to render real elements. This is the standard Angular pattern for testing layout wrappers in isolation. + - [ ] **Step 2: Run the test to verify it passes** Run: `npx nx test cockpit-chat-generative-ui-angular --test-path-pattern=container.component.spec` @@ -190,10 +212,24 @@ Create `cockpit/chat/generative-ui/angular/src/app/views/dashboard-grid.componen ```ts // SPDX-License-Identifier: MIT -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, input } from '@angular/core'; import type { Spec } from '@json-render/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RenderElementComponent } from '@ngaf/render'; import { DashboardGridComponent } from './dashboard-grid.component'; +// See ContainerComponent spec for rationale. Same stub pattern keeps Angular +// from instantiating the real (which needs RENDER_CONTEXT). +@Component({ + selector: 'render-element', + standalone: true, + template: '', +}) +class StubRenderElementComponent { + readonly elementKey = input(''); + readonly spec = input(undefined); +} + describe('DashboardGridComponent', () => { let fixture: ComponentFixture; @@ -202,7 +238,12 @@ describe('DashboardGridComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [DashboardGridComponent], - }).compileComponents(); + }) + .overrideComponent(DashboardGridComponent, { + remove: { imports: [RenderElementComponent] }, + add: { imports: [StubRenderElementComponent] }, + }) + .compileComponents(); fixture = TestBed.createComponent(DashboardGridComponent); }); @@ -275,8 +316,8 @@ Run before opening the PR: - [ ] Both new spec files exist at the paths above and import `Spec` from `@json-render/core`. - [ ] Test names describe behavior (layout class, child count), not implementation. -- [ ] No `RenderElementComponent` is imported into the spec — assertions inspect the rendered `` selector strings, not internals. -- [ ] No mocks: `TestBed.configureTestingModule({ imports: [Component] })` is the entire setup, matching `stat-card.component.spec.ts`. +- [ ] `RenderElementComponent` is imported only as the override target so a local `StubRenderElementComponent` (matching selector + public inputs) takes its place — assertions still inspect the rendered `` selector strings. +- [ ] No service mocks; the only test double is the local `StubRenderElementComponent` that shields TestBed from the real component's `RENDER_CONTEXT` injection. - [ ] License header `// SPDX-License-Identifier: MIT` is present on both files. - [ ] `npx nx test cockpit-chat-generative-ui-angular` passes with all view specs green. From 1ffbe90c3b667ebc9b7aa6dac0422cded773a253 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Thu, 30 Apr 2026 17:30:16 -0700 Subject: [PATCH 3/4] test(cockpit): cover ContainerComponent layout and child rendering Co-Authored-By: Claude Sonnet 4.6 --- .../src/app/views/container.component.spec.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 cockpit/chat/generative-ui/angular/src/app/views/container.component.spec.ts diff --git a/cockpit/chat/generative-ui/angular/src/app/views/container.component.spec.ts b/cockpit/chat/generative-ui/angular/src/app/views/container.component.spec.ts new file mode 100644 index 000000000..9f7029a84 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/app/views/container.component.spec.ts @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +import { Component, input } from '@angular/core'; +import type { Spec } from '@json-render/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RenderElementComponent } from '@ngaf/render'; +import { ContainerComponent } from './container.component'; + +// Stub matching 's selector + public inputs. Swapped into +// ContainerComponent's imports via overrideComponent so Angular doesn't +// instantiate the real RenderElementComponent (which requires RENDER_CONTEXT). +@Component({ + selector: 'render-element', + standalone: true, + template: '', +}) +class StubRenderElementComponent { + readonly elementKey = input(''); + readonly spec = input(undefined); +} + +describe('ContainerComponent', () => { + let fixture: ComponentFixture; + + // Minimal Spec satisfying the input.required() — children resolution + // is delegated to , so the spec body itself is not exercised. + const emptySpec: Spec = { elements: {}, root: 'root' }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ContainerComponent], + }) + .overrideComponent(ContainerComponent, { + remove: { imports: [RenderElementComponent] }, + add: { imports: [StubRenderElementComponent] }, + }) + .compileComponents(); + fixture = TestBed.createComponent(ContainerComponent); + }); + + it('applies column flex classes by default', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.detectChanges(); + const wrapper = fixture.nativeElement.querySelector('div'); + expect(wrapper?.className).toContain('flex-col'); + expect(wrapper?.className).not.toContain('flex-row'); + }); + + it('applies row flex classes when direction is "row"', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.componentRef.setInput('direction', 'row'); + fixture.detectChanges(); + const wrapper = fixture.nativeElement.querySelector('div'); + expect(wrapper?.className).toContain('flex-row'); + expect(wrapper?.className).not.toContain('flex-col'); + }); + + it('renders one render-element per childKey', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.componentRef.setInput('childKeys', ['a', 'b', 'c']); + fixture.detectChanges(); + const elements = fixture.nativeElement.querySelectorAll('render-element'); + expect(elements.length).toBe(3); + }); + + it('renders no render-element children when childKeys is empty', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.componentRef.setInput('childKeys', []); + fixture.detectChanges(); + const elements = fixture.nativeElement.querySelectorAll('render-element'); + expect(elements.length).toBe(0); + }); +}); From 52719f48f947bb1edad2d60f67ee8b7457a27f66 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Thu, 30 Apr 2026 17:33:00 -0700 Subject: [PATCH 4/4] test(cockpit): cover DashboardGridComponent layout and child rendering --- .../views/dashboard-grid.component.spec.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 cockpit/chat/generative-ui/angular/src/app/views/dashboard-grid.component.spec.ts diff --git a/cockpit/chat/generative-ui/angular/src/app/views/dashboard-grid.component.spec.ts b/cockpit/chat/generative-ui/angular/src/app/views/dashboard-grid.component.spec.ts new file mode 100644 index 000000000..5556b16b8 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/app/views/dashboard-grid.component.spec.ts @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +import { Component, input } from '@angular/core'; +import type { Spec } from '@json-render/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RenderElementComponent } from '@ngaf/render'; +import { DashboardGridComponent } from './dashboard-grid.component'; + +// See ContainerComponent spec for rationale. Same stub pattern keeps Angular +// from instantiating the real (which needs RENDER_CONTEXT). +@Component({ + selector: 'render-element', + standalone: true, + template: '', +}) +class StubRenderElementComponent { + readonly elementKey = input(''); + readonly spec = input(undefined); +} + +describe('DashboardGridComponent', () => { + let fixture: ComponentFixture; + + const emptySpec: Spec = { elements: {}, root: 'root' }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DashboardGridComponent], + }) + .overrideComponent(DashboardGridComponent, { + remove: { imports: [RenderElementComponent] }, + add: { imports: [StubRenderElementComponent] }, + }) + .compileComponents(); + fixture = TestBed.createComponent(DashboardGridComponent); + }); + + it('applies vertical flex layout with section spacing', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.detectChanges(); + const wrapper = fixture.nativeElement.querySelector('div'); + expect(wrapper?.className).toContain('flex-col'); + expect(wrapper?.className).toContain('gap-6'); + }); + + it('renders one render-element per childKey', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.componentRef.setInput('childKeys', ['stats_row', 'charts_row', 'table_section']); + fixture.detectChanges(); + const elements = fixture.nativeElement.querySelectorAll('render-element'); + expect(elements.length).toBe(3); + }); + + it('renders no render-element children when childKeys is empty', () => { + fixture.componentRef.setInput('spec', emptySpec); + fixture.componentRef.setInput('childKeys', []); + fixture.detectChanges(); + const elements = fixture.nativeElement.querySelectorAll('render-element'); + expect(elements.length).toBe(0); + }); +});