chore: Fixed line endings i tutorials and provided Introduction

This commit is contained in:
2025-11-03 21:46:15 +01:00
parent 03656a1f19
commit 7ef23c2905
10 changed files with 2275 additions and 2076 deletions

199
INTRODUCTION.md Normal file
View File

@@ -0,0 +1,199 @@
# 🧪 Introduction to Test-Driven-UI Development
Heres an introduction to the **TestDrive-UI development philosophy**.
It explains *how to develop UI components the TestDrive-UI way*, why each tool is part of the stack, and links directly to their official projects.
---
### “Design the test first — let the interface emerge”
---
## 🎯 What is TestDrive-UI?
**TestDrive-UI** is a lightweight, browser-first development scaffold for building interactive web components using a **Test-Driven Development (TDD)** workflow.
It provides a reproducible structure thats simple enough for hobby projects, but robust enough for AI-assisted, agent-driven development loops.
> **Core idea:**
> Every UI feature begins as a **behavioral test** — not a design or mockup.
> The implementation grows only to satisfy that test.
> The UI emerges naturally from verified expectations.
---
## 🧱 The Toolchain Overview
| Tool | Purpose | Why its used | Website |
| -------------------------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| 🧩 **[Lit](https://lit.dev/)** | Define Web Components with declarative rendering | Fast, framework-agnostic, minimal build overhead; lets you create reusable, encapsulated components | [https://lit.dev](https://lit.dev) |
| 🧪 **[Mocha](https://mochajs.org/)** | Test runner for JavaScript | Mature, flexible, CLI-friendly — ideal for writing descriptive, behavior-oriented tests (`describe` / `it`) | [https://mochajs.org](https://mochajs.org) |
| 💬 **[Chai](https://www.chaijs.com/)** | Assertion library | Clean, readable syntax for expectations (`expect(value).to.equal(...)`) | [https://www.chaijs.com](https://www.chaijs.com) |
| 🧠 **[JSDOM](https://github.com/jsdom/jsdom)** | Simulated DOM for Node | Lets you run DOM-based UI tests without a browser; deterministic and CI-friendly | [https://github.com/jsdom/jsdom](https://github.com/jsdom/jsdom) |
| ⚡ **[Vite](https://vitejs.dev/)** | Development server & bundler | Instant hot-reloads and minimal config for web-component projects | [https://vitejs.dev](https://vitejs.dev) |
| 🧱 **[Storybook](https://storybook.js.org/)** (optional) | Visual component sandbox | Lets you document, preview, and visually test each component in isolation | [https://storybook.js.org](https://storybook.js.org) |
| 🔬 **[Playwright](https://playwright.dev/)** (optional) | End-to-end browser testing | Adds full browser automation for real UI validation | [https://playwright.dev](https://playwright.dev) |
Together, these tools form the **TestDrive-UI loop**:
```
Requirement → Test (Mocha+Chai) → Implementation (Lit) → Run (Vite) → Refine → Repeat
```
---
## 🧩 How the Development Flow Works
### 1. Define the Behavior
Write a short **requirement statement** or JSON spec describing what the component *should do* — not how it looks.
Example:
> “When the user types in the input field, the greeting updates immediately.”
This requirement drives both test and implementation.
---
### 2. Write the Test First
Create a Mocha test file in `src/components/<component-name>/<component>.test.js`:
```javascript
import "./hello-world.js";
describe("<hello-world>", () => {
it("updates the greeting when user types", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const input = el.shadowRoot.querySelector("input");
input.value = "Agent";
input.dispatchEvent(new Event("input"));
await el.updateComplete;
const greeting = el.shadowRoot.querySelector(".greeting");
expect(greeting.textContent.trim()).to.equal("Hello, Agent!");
});
});
```
When you run `npm test`, this test will **fail** until you implement the behavior.
---
### 3. Implement Just Enough Code to Pass
Write the smallest possible component in **Lit** to satisfy the test:
```javascript
import { LitElement, html, css } from "lit";
export class HelloWorld extends LitElement {
static properties = { name: { type: String } };
constructor() { super(); this.name = "World"; }
render() {
return html`
<div class="greeting">Hello, ${this.name}!</div>
<input type="text" .value=${this.name} @input=${this._onInput} />
`;
}
_onInput(e) { this.name = e.target.value; }
}
customElements.define("hello-world", HelloWorld);
```
Run tests again — they should now pass.
---
### 4. Refine, Extend, and Visualize
When a component works, visualize it:
```bash
npm run dev
```
Vite opens the browser instantly so you can adjust styling and layout.
Optionally, use Storybook to display multiple variants:
```javascript
export default { title: "UI/Hello World" };
export const Default = () => `<hello-world></hello-world>`;
export const Named = () => `<hello-world name="Coulomb"></hello-world>`;
```
---
### 5. Iterate
Each new behavior (feature, bugfix, style rule, accessibility improvement) starts with a **new test**.
The system evolves incrementally, each step verifiable by automation.
---
## 🧠 Why This Matters
| Principle | Benefit |
| ----------------------------------------- | ------------------------------------------------ |
| **TDD-first** | Prevents UI drift and regression early. |
| **Declarative UI (Lit)** | Clean separation of state and template. |
| **Deterministic Testing (JSDOM + Mocha)** | Reproducible results without browser complexity. |
| **Fast Iteration (Vite)** | Keeps flow tight and interactive. |
| **Optional Visual Layer (Storybook)** | Communication and design documentation built in. |
---
## 🧩 Advanced Extensions
Once the base workflow feels solid, TestDrive-UI scales elegantly to more advanced cases:
* **Shared reactive stores** — implement central state management (see Tutorial 6).
* **Persistence** — save data using localStorage or IndexedDB (see Tutorial 7).
* **Agent automation** — use AI coding agents to generate and maintain tests automatically (see Tutorial 8).
* **End-to-end testing** — run full browser simulations via Playwright.
---
## 🔗 Official Tool Links
| Tool | Website |
| ---------- | ---------------------------------------------------------------- |
| Lit | [https://lit.dev](https://lit.dev) |
| Mocha | [https://mochajs.org](https://mochajs.org) |
| Chai | [https://www.chaijs.com](https://www.chaijs.com) |
| JSDOM | [https://github.com/jsdom/jsdom](https://github.com/jsdom/jsdom) |
| Vite | [https://vitejs.dev](https://vitejs.dev) |
| Storybook | [https://storybook.js.org](https://storybook.js.org) |
| Playwright | [https://playwright.dev](https://playwright.dev) |
---
## 🚀 Quick Start Recap
```bash
# Clone or unzip scaffold
npm install
# Run tests
npm test
# Start live preview
npm run dev
```
* Tests define behavior.
* Components evolve from tests.
* UIs emerge naturally from verified code.
> **TestDrive-UI** isnt a framework — its a *method*.
> Its how you and your coding agents learn to reason about interfaces through evidence.
---
Start with our tutorials to get going.
xxx

View File

@@ -1,144 +1,144 @@
List of **next-stage tutorial ideas**, organized by **theme** and **complexity**, to help evolve both the teaching path and the framework.
---
## 🧩 **A. UI and Interaction Patterns**
1. **Tutorial 9 — Forms and Validation**
* Build a `<user-form>` component with multiple fields and validation logic.
* Introduce unit tests for validation rules and error display.
* Show how to test async validations (e.g., simulated API check).
2. **Tutorial 10 — Modal Dialogs and Overlays**
* Implement a `<modal-dialog>` web component using Lit and CSS transitions.
* Demonstrate accessibility (`aria-*`) and keyboard interactions.
* TDD focus: test open/close state, keyboard handling, and backdrop clicks.
3. **Tutorial 11 — Keyboard Navigation and Accessibility**
* Add keyboard shortcuts and focus management to existing components.
* Test tab order, focus restoration, and ARIA attributes using jsdom.
4. **Tutorial 12 — Dynamic Lists and Reordering**
* Build a draggable `<sortable-list>` component.
* Test DOM updates, drag/drop events, and final order assertions.
---
## ⚙️ **B. State, Data, and Logic**
5. **Tutorial 13 — Derived and Computed State**
* Extend the store to include computed values (e.g., counts, filters).
* Teach memoization and how to test reactive derivations.
6. **Tutorial 14 — Undo/Redo and Time Travel**
* Add a store history stack and commands for undo/redo.
* Test state rollbacks deterministically.
7. **Tutorial 15 — Multi-Store Coordination**
* Split state across multiple stores (e.g., `UserStore`, `SettingsStore`).
* Show how to compose subscriptions and test inter-store dependencies.
8. **Tutorial 16 — Data Fetching and Caching**
* Use `fetch()` to load data asynchronously and cache it in the store.
* Mock HTTP requests in tests.
* Discuss retry and error handling patterns.
---
## 💎 **C. Styling, Theming, and Branding**
9. **Tutorial 17 — Theming and CSS Variables**
* Implement a theme manager (light/dark/custom colors).
* Test dynamic theme changes via store and DOM styles.
10. **Tutorial 18 — Component Libraries and Design Tokens**
* Introduce reusable style tokens and component variants.
* Build visual regression tests using Storybook snapshots.
---
## 🧠 **D. Architecture and Automation**
11. **Tutorial 19 — Reactive Controllers and Composition**
* Use Lits `ReactiveController` pattern to encapsulate logic like stores or timers.
* TDD pattern: one controller, multiple components.
12. **Tutorial 20 — Custom Build and Test Pipelines**
* Show how to integrate Mocha tests into CI (GitHub Actions).
* Include code coverage (nyc) and automatic agent test reporting.
13. **Tutorial 21 — Agentic Refactoring**
* Demonstrate agents proposing architectural changes:
* identifying code smells,
* extracting controllers,
* enforcing consistent store usage.
14. **Tutorial 22 — Agent-Driven Story Generation**
* Agents create new Storybook stories based on test cases.
* Bridge test specs and UX documentation automatically.
---
## 🔮 **E. Integration and Expansion**
15. **Tutorial 23 — REST and GraphQL API Integration**
* Fetch and render real backend data.
* Mock responses for offline testing.
* Introduce contract tests for schema validation.
16. **Tutorial 24 — Internationalization (i18n)**
* Add locale switching to greetings.
* Test translation loading and pluralization behavior.
17. **Tutorial 25 — Progressive Web App (PWA) Integration**
* Cache store data offline.
* Add service worker tests for persistence.
18. **Tutorial 26 — Performance and Profiling**
* Measure component render times.
* Write regression tests for performance thresholds.
---
## 🚀 **F. Visionary / Meta Layer**
19. **Tutorial 27 — Visual AI Testing**
* Integrate image snapshots from Storybook for visual regression comparison.
* Show how agents can detect UI drift.
20. **Tutorial 28 — Self-Testing Components**
* Each component ships with its own self-test harness.
* Components can verify their own integrity in isolation.
21. **Tutorial 29 — AI-Assisted UX Heuristics**
* Agents analyze user interaction logs and propose UI simplifications.
* TDD for heuristic improvements.
22. **Tutorial 30 — Meta-Framework Evolution**
* Turn TestDrive-UI into a reusable CLI (`npx testdrive-ui new <component>`).
* Generate scaffolds, tests, and stories automatically.
xxx
List of **next-stage tutorial ideas**, organized by **theme** and **complexity**, to help evolve both the teaching path and the framework.
---
## 🧩 **A. UI and Interaction Patterns**
1. **Tutorial 9 — Forms and Validation**
* Build a `<user-form>` component with multiple fields and validation logic.
* Introduce unit tests for validation rules and error display.
* Show how to test async validations (e.g., simulated API check).
2. **Tutorial 10 — Modal Dialogs and Overlays**
* Implement a `<modal-dialog>` web component using Lit and CSS transitions.
* Demonstrate accessibility (`aria-*`) and keyboard interactions.
* TDD focus: test open/close state, keyboard handling, and backdrop clicks.
3. **Tutorial 11 — Keyboard Navigation and Accessibility**
* Add keyboard shortcuts and focus management to existing components.
* Test tab order, focus restoration, and ARIA attributes using jsdom.
4. **Tutorial 12 — Dynamic Lists and Reordering**
* Build a draggable `<sortable-list>` component.
* Test DOM updates, drag/drop events, and final order assertions.
---
## ⚙️ **B. State, Data, and Logic**
5. **Tutorial 13 — Derived and Computed State**
* Extend the store to include computed values (e.g., counts, filters).
* Teach memoization and how to test reactive derivations.
6. **Tutorial 14 — Undo/Redo and Time Travel**
* Add a store history stack and commands for undo/redo.
* Test state rollbacks deterministically.
7. **Tutorial 15 — Multi-Store Coordination**
* Split state across multiple stores (e.g., `UserStore`, `SettingsStore`).
* Show how to compose subscriptions and test inter-store dependencies.
8. **Tutorial 16 — Data Fetching and Caching**
* Use `fetch()` to load data asynchronously and cache it in the store.
* Mock HTTP requests in tests.
* Discuss retry and error handling patterns.
---
## 💎 **C. Styling, Theming, and Branding**
9. **Tutorial 17 — Theming and CSS Variables**
* Implement a theme manager (light/dark/custom colors).
* Test dynamic theme changes via store and DOM styles.
10. **Tutorial 18 — Component Libraries and Design Tokens**
* Introduce reusable style tokens and component variants.
* Build visual regression tests using Storybook snapshots.
---
## 🧠 **D. Architecture and Automation**
11. **Tutorial 19 — Reactive Controllers and Composition**
* Use Lits `ReactiveController` pattern to encapsulate logic like stores or timers.
* TDD pattern: one controller, multiple components.
12. **Tutorial 20 — Custom Build and Test Pipelines**
* Show how to integrate Mocha tests into CI (GitHub Actions).
* Include code coverage (nyc) and automatic agent test reporting.
13. **Tutorial 21 — Agentic Refactoring**
* Demonstrate agents proposing architectural changes:
* identifying code smells,
* extracting controllers,
* enforcing consistent store usage.
14. **Tutorial 22 — Agent-Driven Story Generation**
* Agents create new Storybook stories based on test cases.
* Bridge test specs and UX documentation automatically.
---
## 🔮 **E. Integration and Expansion**
15. **Tutorial 23 — REST and GraphQL API Integration**
* Fetch and render real backend data.
* Mock responses for offline testing.
* Introduce contract tests for schema validation.
16. **Tutorial 24 — Internationalization (i18n)**
* Add locale switching to greetings.
* Test translation loading and pluralization behavior.
17. **Tutorial 25 — Progressive Web App (PWA) Integration**
* Cache store data offline.
* Add service worker tests for persistence.
18. **Tutorial 26 — Performance and Profiling**
* Measure component render times.
* Write regression tests for performance thresholds.
---
## 🚀 **F. Visionary / Meta Layer**
19. **Tutorial 27 — Visual AI Testing**
* Integrate image snapshots from Storybook for visual regression comparison.
* Show how agents can detect UI drift.
20. **Tutorial 28 — Self-Testing Components**
* Each component ships with its own self-test harness.
* Components can verify their own integrity in isolation.
21. **Tutorial 29 — AI-Assisted UX Heuristics**
* Agents analyze user interaction logs and propose UI simplifications.
* TDD for heuristic improvements.
22. **Tutorial 30 — Meta-Framework Evolution**
* Turn TestDrive-UI into a reusable CLI (`npx testdrive-ui new <component>`).
* Generate scaffolds, tests, and stories automatically.
xxx

View File

@@ -1,187 +1,187 @@
# Tutorial 1: Hello World!
Lets walk through your first **HelloWorld** component built with **TestDrive-UI**.
This will demonstrate the full workflow:
👉 requirement → test → implementation → run → refinement.
---
## 🧱 1. Project setup
If you havent already:
```bash
unzip testdrive-ui.zip
cd testdrive-ui
npm install
```
Now youre ready to create your first component.
---
## 🧩 2. Create a component folder
```bash
mkdir src/components/hello-world
```
Inside it, youll have:
```
hello-world/
├── hello-world.js
├── hello-world.test.js
└── hello-world.stories.js
```
---
## ✏️ 3. Step 1 — Write the requirement
> “When `<hello-world>` is rendered, it should display the text **Hello World!**
> and clicking the element should show an alert.”
Thats your behavioral spec — the *“why”* that drives the test.
---
## 🧪 4. Step 2 — Write the test first
`src/components/hello-world/hello-world.test.js`
```javascript
import "./hello-world.js";
describe("<hello-world>", () => {
it("renders the correct greeting", () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const content = el.shadowRoot.textContent;
expect(content).to.include("Hello World!");
});
it("triggers an alert on click", () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
let alerted = false;
window.alert = () => (alerted = true);
const div = el.shadowRoot.querySelector("div");
div.click();
expect(alerted).to.be.true;
});
});
```
Run this test now:
```bash
npm test
```
Both tests will **fail** initially — perfect, thats the TDD start.
---
## 💡 5. Step 3 — Implement the component
`src/components/hello-world/hello-world.js`
```javascript
import { LitElement, html, css } from "lit";
export class HelloWorld extends LitElement {
static styles = css`
div {
font-family: system-ui, sans-serif;
font-size: 1.5rem;
color: #007acc;
padding: 1rem;
text-align: center;
cursor: pointer;
user-select: none;
}
div:hover {
color: #005fa3;
}
`;
render() {
return html`<div @click=${this._onClick}>Hello World!</div>`;
}
_onClick() {
alert("Hello from TestDrive-UI!");
}
}
customElements.define("hello-world", HelloWorld);
```
Run the tests again:
```bash
npm test
```
✅ Both should now pass.
---
## ⚡ 6. Step 4 — Preview it in the browser
Add it to your `index.html`:
```html
<script type="module" src="./components/hello-world/hello-world.js"></script>
<hello-world></hello-world>
```
Then run:
```bash
npm run dev
```
Open [http://localhost:5173](http://localhost:5173)
→ Youll see your clickable **Hello World!** component rendered live.
---
## 🧭 7. Step 5 — Visual story (optional)
`src/components/hello-world/hello-world.stories.js`
```javascript
import "./hello-world.js";
export default {
title: "UI/Hello World",
};
export const Default = () => `<hello-world></hello-world>`;
```
Once you add Storybook to the scaffold later, this file will automatically generate a visual preview card.
---
## ✅ 8. What you achieved
* Created an **independent UI component** with Lit
* Wrote deterministic **tests with jsdom + Mocha**
* Verified behavior automatically
* Previewed visually via **Vite**
This loop is your core **TestDrive-UI workflow**:
```
spec → test → implement → run → refine
```
xxx
# Tutorial 1: Hello World!
Lets walk through your first **HelloWorld** component built with **TestDrive-UI**.
This will demonstrate the full workflow:
👉 requirement → test → implementation → run → refinement.
---
## 🧱 1. Project setup
If you havent already:
```bash
unzip testdrive-ui.zip
cd testdrive-ui
npm install
```
Now youre ready to create your first component.
---
## 🧩 2. Create a component folder
```bash
mkdir src/components/hello-world
```
Inside it, youll have:
```
hello-world/
├── hello-world.js
├── hello-world.test.js
└── hello-world.stories.js
```
---
## ✏️ 3. Step 1 — Write the requirement
> “When `<hello-world>` is rendered, it should display the text **Hello World!**
> and clicking the element should show an alert.”
Thats your behavioral spec — the *“why”* that drives the test.
---
## 🧪 4. Step 2 — Write the test first
`src/components/hello-world/hello-world.test.js`
```javascript
import "./hello-world.js";
describe("<hello-world>", () => {
it("renders the correct greeting", () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const content = el.shadowRoot.textContent;
expect(content).to.include("Hello World!");
});
it("triggers an alert on click", () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
let alerted = false;
window.alert = () => (alerted = true);
const div = el.shadowRoot.querySelector("div");
div.click();
expect(alerted).to.be.true;
});
});
```
Run this test now:
```bash
npm test
```
Both tests will **fail** initially — perfect, thats the TDD start.
---
## 💡 5. Step 3 — Implement the component
`src/components/hello-world/hello-world.js`
```javascript
import { LitElement, html, css } from "lit";
export class HelloWorld extends LitElement {
static styles = css`
div {
font-family: system-ui, sans-serif;
font-size: 1.5rem;
color: #007acc;
padding: 1rem;
text-align: center;
cursor: pointer;
user-select: none;
}
div:hover {
color: #005fa3;
}
`;
render() {
return html`<div @click=${this._onClick}>Hello World!</div>`;
}
_onClick() {
alert("Hello from TestDrive-UI!");
}
}
customElements.define("hello-world", HelloWorld);
```
Run the tests again:
```bash
npm test
```
✅ Both should now pass.
---
## ⚡ 6. Step 4 — Preview it in the browser
Add it to your `index.html`:
```html
<script type="module" src="./components/hello-world/hello-world.js"></script>
<hello-world></hello-world>
```
Then run:
```bash
npm run dev
```
Open [http://localhost:5173](http://localhost:5173)
→ Youll see your clickable **Hello World!** component rendered live.
---
## 🧭 7. Step 5 — Visual story (optional)
`src/components/hello-world/hello-world.stories.js`
```javascript
import "./hello-world.js";
export default {
title: "UI/Hello World",
};
export const Default = () => `<hello-world></hello-world>`;
```
Once you add Storybook to the scaffold later, this file will automatically generate a visual preview card.
---
## ✅ 8. What you achieved
* Created an **independent UI component** with Lit
* Wrote deterministic **tests with jsdom + Mocha**
* Verified behavior automatically
* Previewed visually via **Vite**
This loop is your core **TestDrive-UI workflow**:
```
spec → test → implement → run → refine
```
xxx

View File

@@ -1,217 +1,217 @@
# Tutorial 2 Reactive Properties
This second **TestDrive-UI** tutorial extends your previous `hello-world` component by introducing **reactive properties** (i.e., component inputs) and **dynamic rendering**, all under **TDD** control.
Well end up with a `<hello-world name="Kale"></hello-world>` component that greets a given name — and can change dynamically when the property updates.
---
## 🧭 1. Goal
> The component should display “Hello, [name]!”
> and automatically update when the `name` property changes.
If no `name` is given, it should default to “World”.
---
## 🧪 2. Step 1 — Write the failing test first
Create `src/components/hello-world/hello-world.props.test.js`:
```javascript
import "./hello-world.js";
describe("<hello-world> (with name property)", () => {
it("renders default greeting when no name is set", () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const text = el.shadowRoot.textContent.trim();
expect(text).to.equal("Hello, World!");
});
it("renders custom greeting when name is set", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
el.name = "Kale";
// Wait for Lits update cycle
await el.updateComplete;
const text = el.shadowRoot.textContent.trim();
expect(text).to.equal("Hello, Kale!");
});
it("reacts to property change after initial render", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
el.name = "Aria";
await el.updateComplete;
let text = el.shadowRoot.textContent.trim();
expect(text).to.equal("Hello, Aria!");
el.name = "Nova";
await el.updateComplete;
text = el.shadowRoot.textContent.trim();
expect(text).to.equal("Hello, Nova!");
});
});
```
Run:
```bash
npm test
```
All tests should fail — we havent implemented anything yet.
---
## 🧩 3. Step 2 — Implement the feature
Open your existing `src/components/hello-world/hello-world.js`
and replace the class with this improved version:
```javascript
import { LitElement, html, css } from "lit";
export class HelloWorld extends LitElement {
static properties = {
name: { type: String }
};
constructor() {
super();
this.name = "World";
}
static styles = css`
div {
font-family: system-ui, sans-serif;
font-size: 1.5rem;
color: #007acc;
padding: 1rem;
text-align: center;
cursor: pointer;
user-select: none;
}
div:hover {
color: #005fa3;
}
`;
render() {
return html`<div @click=${this._onClick}>
Hello, ${this.name}!
</div>`;
}
_onClick() {
alert(`Hello, ${this.name}!`);
}
}
customElements.define("hello-world", HelloWorld);
```
Run the tests again:
```bash
npm test
```
✅ All should now pass.
---
## ⚡ 4. Step 3 — Try it live
Edit `src/index.html` to demonstrate both variants:
```html
<hello-world></hello-world>
<hello-world name="Coulomb"></hello-world>
```
Then:
```bash
npm run dev
```
In the browser youll see:
```
Hello, World!
Hello, Coulomb!
```
and both are clickable.
---
## 🔄 5. Step 4 — Live updates (optional exploration)
Open the browser console and type:
```javascript
document.querySelector("hello-world").name = "Agent";
```
The first greeting should **update instantly** to:
```
Hello, Agent!
```
Thats Lits reactive update mechanism at work.
---
## 🧭 6. Step 5 — Visual story (optional)
`src/components/hello-world/hello-world.stories.js`
```javascript
import "./hello-world.js";
export default {
title: "UI/Hello World (Reactive)"
};
export const Default = () => `<hello-world></hello-world>`;
export const CustomName = () => `<hello-world name="Bernd"></hello-world>`;
```
If Storybook is later installed, these stories will become live demos.
---
## 🧩 7. Key Takeaways
| Concept | Explanation |
| --------------------- | ------------------------------------------------- |
| **Reactive property** | Declared via `static properties = { ... }` in Lit |
| **Default values** | Set in the constructor |
| **Automatic updates** | Changing the property triggers re-render |
| **Testing updates** | Use `await el.updateComplete` before asserting |
---
## 🧪 8. What you learned
* How to **declare reactive component properties**
* How to **test reactivity** with Mocha + jsdom
* How to **update and verify UI behavior** in a TDD loop
---
Next, we can take it one level further:
> Add a **text input** inside `<hello-world>` that updates the `name` property live when the user types.
# Tutorial 2 Reactive Properties
This second **TestDrive-UI** tutorial extends your previous `hello-world` component by introducing **reactive properties** (i.e., component inputs) and **dynamic rendering**, all under **TDD** control.
Well end up with a `<hello-world name="Kale"></hello-world>` component that greets a given name — and can change dynamically when the property updates.
---
## 🧭 1. Goal
> The component should display “Hello, [name]!”
> and automatically update when the `name` property changes.
If no `name` is given, it should default to “World”.
---
## 🧪 2. Step 1 — Write the failing test first
Create `src/components/hello-world/hello-world.props.test.js`:
```javascript
import "./hello-world.js";
describe("<hello-world> (with name property)", () => {
it("renders default greeting when no name is set", () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const text = el.shadowRoot.textContent.trim();
expect(text).to.equal("Hello, World!");
});
it("renders custom greeting when name is set", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
el.name = "Kale";
// Wait for Lits update cycle
await el.updateComplete;
const text = el.shadowRoot.textContent.trim();
expect(text).to.equal("Hello, Kale!");
});
it("reacts to property change after initial render", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
el.name = "Aria";
await el.updateComplete;
let text = el.shadowRoot.textContent.trim();
expect(text).to.equal("Hello, Aria!");
el.name = "Nova";
await el.updateComplete;
text = el.shadowRoot.textContent.trim();
expect(text).to.equal("Hello, Nova!");
});
});
```
Run:
```bash
npm test
```
All tests should fail — we havent implemented anything yet.
---
## 🧩 3. Step 2 — Implement the feature
Open your existing `src/components/hello-world/hello-world.js`
and replace the class with this improved version:
```javascript
import { LitElement, html, css } from "lit";
export class HelloWorld extends LitElement {
static properties = {
name: { type: String }
};
constructor() {
super();
this.name = "World";
}
static styles = css`
div {
font-family: system-ui, sans-serif;
font-size: 1.5rem;
color: #007acc;
padding: 1rem;
text-align: center;
cursor: pointer;
user-select: none;
}
div:hover {
color: #005fa3;
}
`;
render() {
return html`<div @click=${this._onClick}>
Hello, ${this.name}!
</div>`;
}
_onClick() {
alert(`Hello, ${this.name}!`);
}
}
customElements.define("hello-world", HelloWorld);
```
Run the tests again:
```bash
npm test
```
✅ All should now pass.
---
## ⚡ 4. Step 3 — Try it live
Edit `src/index.html` to demonstrate both variants:
```html
<hello-world></hello-world>
<hello-world name="Coulomb"></hello-world>
```
Then:
```bash
npm run dev
```
In the browser youll see:
```
Hello, World!
Hello, Coulomb!
```
and both are clickable.
---
## 🔄 5. Step 4 — Live updates (optional exploration)
Open the browser console and type:
```javascript
document.querySelector("hello-world").name = "Agent";
```
The first greeting should **update instantly** to:
```
Hello, Agent!
```
Thats Lits reactive update mechanism at work.
---
## 🧭 6. Step 5 — Visual story (optional)
`src/components/hello-world/hello-world.stories.js`
```javascript
import "./hello-world.js";
export default {
title: "UI/Hello World (Reactive)"
};
export const Default = () => `<hello-world></hello-world>`;
export const CustomName = () => `<hello-world name="Bernd"></hello-world>`;
```
If Storybook is later installed, these stories will become live demos.
---
## 🧩 7. Key Takeaways
| Concept | Explanation |
| --------------------- | ------------------------------------------------- |
| **Reactive property** | Declared via `static properties = { ... }` in Lit |
| **Default values** | Set in the constructor |
| **Automatic updates** | Changing the property triggers re-render |
| **Testing updates** | Use `await el.updateComplete` before asserting |
---
## 🧪 8. What you learned
* How to **declare reactive component properties**
* How to **test reactivity** with Mocha + jsdom
* How to **update and verify UI behavior** in a TDD loop
---
Next, we can take it one level further:
> Add a **text input** inside `<hello-world>` that updates the `name` property live when the user types.
xxx

View File

@@ -1,221 +1,221 @@
# Tutorial 3: Bi-Directional Data Binding
This **third tutorial** builds directly on your `hello-world` component and introduces **two-way interaction** (bi-directional data binding).
Youll learn how to let the user type into an input field and see the greeting update live — while continuing to follow a **TDD-first** approach.
---
## 🧭 1. Goal
> The `<hello-world>` component should display a greeting and an input box.
> Typing into the input should update the greeting **in real time**.
> The `name` property should still be readable/writable programmatically.
---
## 🧪 2. Step 1 — Write the failing test first
Create a new test file:
`src/components/hello-world/hello-world.input.test.js`
```javascript
import "./hello-world.js";
describe("<hello-world> (interactive input)", () => {
it("renders an input element and shows default greeting", () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const input = el.shadowRoot.querySelector("input");
const greeting = el.shadowRoot.querySelector(".greeting");
expect(input).to.exist;
expect(greeting.textContent.trim()).to.equal("Hello, World!");
});
it("updates greeting when user types into input", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const input = el.shadowRoot.querySelector("input");
input.value = "Agent";
input.dispatchEvent(new Event("input"));
await el.updateComplete;
const greeting = el.shadowRoot.querySelector(".greeting");
expect(greeting.textContent.trim()).to.equal("Hello, Agent!");
});
it("reflects property changes in input value", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
el.name = "Nova";
await el.updateComplete;
const input = el.shadowRoot.querySelector("input");
expect(input.value).to.equal("Nova");
});
});
```
Run tests:
```bash
npm test
```
They will all **fail** — as expected.
---
## 🧩 3. Step 2 — Implement the feature
Edit `src/components/hello-world/hello-world.js` and replace the render logic:
```javascript
import { LitElement, html, css } from "lit";
export class HelloWorld extends LitElement {
static properties = {
name: { type: String }
};
constructor() {
super();
this.name = "World";
}
static styles = css`
.container {
font-family: system-ui, sans-serif;
font-size: 1.5rem;
color: #007acc;
padding: 1rem;
text-align: center;
}
input {
font-size: 1rem;
margin-top: 1rem;
padding: 0.3rem 0.6rem;
border: 1px solid #ccc;
border-radius: 6px;
width: 60%;
text-align: center;
}
`;
render() {
return html`
<div class="container">
<div class="greeting">Hello, ${this.name}!</div>
<input
type="text"
.value=${this.name}
@input=${this._onInput}
aria-label="Name input"
/>
</div>
`;
}
_onInput(event) {
this.name = event.target.value;
}
}
customElements.define("hello-world", HelloWorld);
```
---
## 🧪 4. Step 3 — Run the tests again
```bash
npm test
```
✅ All three tests should now pass.
---
## ⚡ 5. Step 4 — Try it live
Update `src/index.html` to:
```html
<hello-world></hello-world>
```
Run:
```bash
npm run dev
```
In your browser:
* Youll see “Hello, World!”
* Type “Coulomb” in the input box.
* The greeting updates instantly: **Hello, Coulomb!**
---
## 🧠 6. Step 5 — Explore two-way binding manually
Open DevTools Console and run:
```javascript
document.querySelector("hello-world").name = "Bernd";
```
The input value updates automatically, and the greeting reflects the change too.
Thats the power of Lits **reactive updates** combined with the native DOM event loop.
---
## 📚 7. Step 6 — Optional Storybook story
`src/components/hello-world/hello-world.interactive.stories.js`
```javascript
import "./hello-world.js";
export default {
title: "UI/Hello World (Interactive)"
};
export const Interactive = () => `<hello-world></hello-world>`;
```
When you add Storybook later, this story will provide a live, interactive playground.
---
## 🔍 8. Key TDD lessons learned
| Concept | Explanation |
| -------------------- | ------------------------------------------------------------ |
| **Event testing** | Simulate user input with `dispatchEvent(new Event('input'))` |
| **Reactive updates** | Use `await el.updateComplete` to wait for re-render |
| **Two-way binding** | Property changes update DOM; DOM events update property |
| **Isolation** | jsdom tests confirm behavior without running a browser |
---
## ✅ 9. Summary
Youve now completed:
1. Static rendering (`Hello World!`)
2. Reactive property rendering (`Hello, ${name}!`)
3. Bi-directional interaction (live input → UI → property → UI)
You can now **test-drive** any UI component using the same methodology.
---
Continue to the **fourth tutorial** on **component composition** — i.e., building a small dashboard that uses multiple custom components together, still under test Control.
# Tutorial 3: Bi-Directional Data Binding
This **third tutorial** builds directly on your `hello-world` component and introduces **two-way interaction** (bi-directional data binding).
Youll learn how to let the user type into an input field and see the greeting update live — while continuing to follow a **TDD-first** approach.
---
## 🧭 1. Goal
> The `<hello-world>` component should display a greeting and an input box.
> Typing into the input should update the greeting **in real time**.
> The `name` property should still be readable/writable programmatically.
---
## 🧪 2. Step 1 — Write the failing test first
Create a new test file:
`src/components/hello-world/hello-world.input.test.js`
```javascript
import "./hello-world.js";
describe("<hello-world> (interactive input)", () => {
it("renders an input element and shows default greeting", () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const input = el.shadowRoot.querySelector("input");
const greeting = el.shadowRoot.querySelector(".greeting");
expect(input).to.exist;
expect(greeting.textContent.trim()).to.equal("Hello, World!");
});
it("updates greeting when user types into input", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const input = el.shadowRoot.querySelector("input");
input.value = "Agent";
input.dispatchEvent(new Event("input"));
await el.updateComplete;
const greeting = el.shadowRoot.querySelector(".greeting");
expect(greeting.textContent.trim()).to.equal("Hello, Agent!");
});
it("reflects property changes in input value", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
el.name = "Nova";
await el.updateComplete;
const input = el.shadowRoot.querySelector("input");
expect(input.value).to.equal("Nova");
});
});
```
Run tests:
```bash
npm test
```
They will all **fail** — as expected.
---
## 🧩 3. Step 2 — Implement the feature
Edit `src/components/hello-world/hello-world.js` and replace the render logic:
```javascript
import { LitElement, html, css } from "lit";
export class HelloWorld extends LitElement {
static properties = {
name: { type: String }
};
constructor() {
super();
this.name = "World";
}
static styles = css`
.container {
font-family: system-ui, sans-serif;
font-size: 1.5rem;
color: #007acc;
padding: 1rem;
text-align: center;
}
input {
font-size: 1rem;
margin-top: 1rem;
padding: 0.3rem 0.6rem;
border: 1px solid #ccc;
border-radius: 6px;
width: 60%;
text-align: center;
}
`;
render() {
return html`
<div class="container">
<div class="greeting">Hello, ${this.name}!</div>
<input
type="text"
.value=${this.name}
@input=${this._onInput}
aria-label="Name input"
/>
</div>
`;
}
_onInput(event) {
this.name = event.target.value;
}
}
customElements.define("hello-world", HelloWorld);
```
---
## 🧪 4. Step 3 — Run the tests again
```bash
npm test
```
✅ All three tests should now pass.
---
## ⚡ 5. Step 4 — Try it live
Update `src/index.html` to:
```html
<hello-world></hello-world>
```
Run:
```bash
npm run dev
```
In your browser:
* Youll see “Hello, World!”
* Type “Coulomb” in the input box.
* The greeting updates instantly: **Hello, Coulomb!**
---
## 🧠 6. Step 5 — Explore two-way binding manually
Open DevTools Console and run:
```javascript
document.querySelector("hello-world").name = "Bernd";
```
The input value updates automatically, and the greeting reflects the change too.
Thats the power of Lits **reactive updates** combined with the native DOM event loop.
---
## 📚 7. Step 6 — Optional Storybook story
`src/components/hello-world/hello-world.interactive.stories.js`
```javascript
import "./hello-world.js";
export default {
title: "UI/Hello World (Interactive)"
};
export const Interactive = () => `<hello-world></hello-world>`;
```
When you add Storybook later, this story will provide a live, interactive playground.
---
## 🔍 8. Key TDD lessons learned
| Concept | Explanation |
| -------------------- | ------------------------------------------------------------ |
| **Event testing** | Simulate user input with `dispatchEvent(new Event('input'))` |
| **Reactive updates** | Use `await el.updateComplete` to wait for re-render |
| **Two-way binding** | Property changes update DOM; DOM events update property |
| **Isolation** | jsdom tests confirm behavior without running a browser |
---
## ✅ 9. Summary
Youve now completed:
1. Static rendering (`Hello World!`)
2. Reactive property rendering (`Hello, ${name}!`)
3. Bi-directional interaction (live input → UI → property → UI)
You can now **test-drive** any UI component using the same methodology.
---
Continue to the **fourth tutorial** on **component composition** — i.e., building a small dashboard that uses multiple custom components together, still under test Control.
xxx

View File

@@ -1,257 +1,257 @@
# Tutorial 4: Component Composition
Welcome to the **fourth tutorial** for TestDriveUi building on the first three and introduces **component composition**.
Youll learn how to make multiple Lit components **interact** while keeping the development **test-driven** and agent-friendly.
---
# 🧩 Tutorial 4 — Composing Components
### “Hello Dashboard” — combining reusable components
---
## 🎯 Goal
Well create a small **dashboard** component called `<hello-dashboard>` that:
1. Renders multiple `<hello-world>` components.
2. Tracks how many greetings are currently visible.
3. Updates a counter when a new greeter is added.
4. Uses **TDD** for structure and behavior.
In short:
> “A dashboard that shows several personalized greetings and a live counter.”
---
## 🧪 1. Step 1 — Write the failing test first
Create `src/components/hello-dashboard/hello-dashboard.test.js`:
```javascript
import "../hello-world/hello-world.js";
import "./hello-dashboard.js";
describe("<hello-dashboard>", () => {
it("renders a header and a counter", () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
const header = el.shadowRoot.querySelector("h2");
const counter = el.shadowRoot.querySelector(".counter");
expect(header.textContent).to.include("Hello Dashboard");
expect(counter.textContent).to.match(/Total greetings:/);
});
it("renders at least one <hello-world> component", () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
const greeters = el.shadowRoot.querySelectorAll("hello-world");
expect(greeters.length).to.be.greaterThan(0);
});
it("adds a new greeter when the Add button is clicked", async () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
const addButton = el.shadowRoot.querySelector("button");
addButton.click();
await el.updateComplete;
const greeters = el.shadowRoot.querySelectorAll("hello-world");
expect(greeters.length).to.equal(2);
const counter = el.shadowRoot.querySelector(".counter").textContent;
expect(counter).to.include("2");
});
});
```
Run it:
```bash
npm test
```
All tests fail (expected).
---
## ⚙️ 2. Step 2 — Implement `<hello-dashboard>`
Create a new folder:
```
src/components/hello-dashboard/
```
and add this file:
`src/components/hello-dashboard/hello-dashboard.js`
```javascript
import { LitElement, html, css } from "lit";
import "../hello-world/hello-world.js";
export class HelloDashboard extends LitElement {
static styles = css`
.container {
font-family: system-ui, sans-serif;
padding: 1rem;
text-align: center;
}
.counter {
margin: 0.5rem 0 1rem 0;
font-weight: bold;
color: #007acc;
}
button {
background: #007acc;
color: white;
border: none;
border-radius: 6px;
padding: 0.4rem 1rem;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
button:hover {
background: #005fa3;
}
`;
constructor() {
super();
this.greeters = ["World"];
}
_addGreeter() {
const newName = `User${this.greeters.length + 1}`;
this.greeters = [...this.greeters, newName];
}
render() {
return html`
<div class="container">
<h2>Hello Dashboard</h2>
<div class="counter">Total greetings: ${this.greeters.length}</div>
${this.greeters.map(
(name) => html`<hello-world name=${name}></hello-world>`
)}
<button @click=${this._addGreeter}>Add Greeting</button>
</div>
`;
}
}
customElements.define("hello-dashboard", HelloDashboard);
```
Run tests again:
```bash
npm test
```
✅ All should now pass.
---
## 🌐 3. Step 3 — Try it live
Add this to `src/index.html`:
```html
<script type="module" src="./components/hello-dashboard/hello-dashboard.js"></script>
<hello-dashboard></hello-dashboard>
```
Then start the dev server:
```bash
npm run dev
```
Open [http://localhost:5173](http://localhost:5173)
Youll see:
```
Hello Dashboard
Total greetings: 1
Hello, World!
[Add Greeting]
```
Click **Add Greeting** — new greeters appear, and the counter updates automatically.
---
## 🧠 4. Step 4 — Extend your test coverage (optional)
Add to `hello-dashboard.test.js`:
```javascript
it("propagates custom names correctly", () => {
const el = document.createElement("hello-dashboard");
el.greeters = ["Alpha", "Beta", "Gamma"];
document.body.appendChild(el);
const greeters = el.shadowRoot.querySelectorAll("hello-world");
const names = Array.from(greeters).map((g) => g.getAttribute("name"));
expect(names).to.deep.equal(["Alpha", "Beta", "Gamma"]);
});
```
---
## 📚 5. Step 5 — Optional Storybook story
`src/components/hello-dashboard/hello-dashboard.stories.js`:
```javascript
import "./hello-dashboard.js";
export default {
title: "UI/Hello Dashboard"
};
export const Default = () => `<hello-dashboard></hello-dashboard>`;
```
---
## 🧩 6. Lessons learned
| Concept | What you practiced |
| --------------------- | ------------------------------------------- |
| **Composition** | One Lit component rendering others |
| **Reactive arrays** | Using property updates to trigger re-render |
| **Dynamic templates** | Rendering lists with `.map()` |
| **Event testing** | Simulating clicks and rechecking the DOM |
| **Incremental TDD** | Adding small features one test at a time |
---
## ✅ 7. Summary
Youve now implemented:
1. A simple reactive component (`hello-world`)
2. User interaction (input updates)
3. Property-based rendering
4. Component composition (`hello-dashboard`)
Your **TestDrive-UI** foundation now supports:
* Isolated unit tests (`Mocha + jsdom`)
* Composed component testing
* Interactive browser previews (`Vite`)
---
Continue to **Event communication** between components — e.g., the dashboard listening to events fired by each greeter when they change their name.
xxx
# Tutorial 4: Component Composition
Welcome to the **fourth tutorial** for TestDriveUi building on the first three and introduces **component composition**.
Youll learn how to make multiple Lit components **interact** while keeping the development **test-driven** and agent-friendly.
---
# 🧩 Tutorial 4 — Composing Components
### “Hello Dashboard” — combining reusable components
---
## 🎯 Goal
Well create a small **dashboard** component called `<hello-dashboard>` that:
1. Renders multiple `<hello-world>` components.
2. Tracks how many greetings are currently visible.
3. Updates a counter when a new greeter is added.
4. Uses **TDD** for structure and behavior.
In short:
> “A dashboard that shows several personalized greetings and a live counter.”
---
## 🧪 1. Step 1 — Write the failing test first
Create `src/components/hello-dashboard/hello-dashboard.test.js`:
```javascript
import "../hello-world/hello-world.js";
import "./hello-dashboard.js";
describe("<hello-dashboard>", () => {
it("renders a header and a counter", () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
const header = el.shadowRoot.querySelector("h2");
const counter = el.shadowRoot.querySelector(".counter");
expect(header.textContent).to.include("Hello Dashboard");
expect(counter.textContent).to.match(/Total greetings:/);
});
it("renders at least one <hello-world> component", () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
const greeters = el.shadowRoot.querySelectorAll("hello-world");
expect(greeters.length).to.be.greaterThan(0);
});
it("adds a new greeter when the Add button is clicked", async () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
const addButton = el.shadowRoot.querySelector("button");
addButton.click();
await el.updateComplete;
const greeters = el.shadowRoot.querySelectorAll("hello-world");
expect(greeters.length).to.equal(2);
const counter = el.shadowRoot.querySelector(".counter").textContent;
expect(counter).to.include("2");
});
});
```
Run it:
```bash
npm test
```
All tests fail (expected).
---
## ⚙️ 2. Step 2 — Implement `<hello-dashboard>`
Create a new folder:
```
src/components/hello-dashboard/
```
and add this file:
`src/components/hello-dashboard/hello-dashboard.js`
```javascript
import { LitElement, html, css } from "lit";
import "../hello-world/hello-world.js";
export class HelloDashboard extends LitElement {
static styles = css`
.container {
font-family: system-ui, sans-serif;
padding: 1rem;
text-align: center;
}
.counter {
margin: 0.5rem 0 1rem 0;
font-weight: bold;
color: #007acc;
}
button {
background: #007acc;
color: white;
border: none;
border-radius: 6px;
padding: 0.4rem 1rem;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
button:hover {
background: #005fa3;
}
`;
constructor() {
super();
this.greeters = ["World"];
}
_addGreeter() {
const newName = `User${this.greeters.length + 1}`;
this.greeters = [...this.greeters, newName];
}
render() {
return html`
<div class="container">
<h2>Hello Dashboard</h2>
<div class="counter">Total greetings: ${this.greeters.length}</div>
${this.greeters.map(
(name) => html`<hello-world name=${name}></hello-world>`
)}
<button @click=${this._addGreeter}>Add Greeting</button>
</div>
`;
}
}
customElements.define("hello-dashboard", HelloDashboard);
```
Run tests again:
```bash
npm test
```
✅ All should now pass.
---
## 🌐 3. Step 3 — Try it live
Add this to `src/index.html`:
```html
<script type="module" src="./components/hello-dashboard/hello-dashboard.js"></script>
<hello-dashboard></hello-dashboard>
```
Then start the dev server:
```bash
npm run dev
```
Open [http://localhost:5173](http://localhost:5173)
Youll see:
```
Hello Dashboard
Total greetings: 1
Hello, World!
[Add Greeting]
```
Click **Add Greeting** — new greeters appear, and the counter updates automatically.
---
## 🧠 4. Step 4 — Extend your test coverage (optional)
Add to `hello-dashboard.test.js`:
```javascript
it("propagates custom names correctly", () => {
const el = document.createElement("hello-dashboard");
el.greeters = ["Alpha", "Beta", "Gamma"];
document.body.appendChild(el);
const greeters = el.shadowRoot.querySelectorAll("hello-world");
const names = Array.from(greeters).map((g) => g.getAttribute("name"));
expect(names).to.deep.equal(["Alpha", "Beta", "Gamma"]);
});
```
---
## 📚 5. Step 5 — Optional Storybook story
`src/components/hello-dashboard/hello-dashboard.stories.js`:
```javascript
import "./hello-dashboard.js";
export default {
title: "UI/Hello Dashboard"
};
export const Default = () => `<hello-dashboard></hello-dashboard>`;
```
---
## 🧩 6. Lessons learned
| Concept | What you practiced |
| --------------------- | ------------------------------------------- |
| **Composition** | One Lit component rendering others |
| **Reactive arrays** | Using property updates to trigger re-render |
| **Dynamic templates** | Rendering lists with `.map()` |
| **Event testing** | Simulating clicks and rechecking the DOM |
| **Incremental TDD** | Adding small features one test at a time |
---
## ✅ 7. Summary
Youve now implemented:
1. A simple reactive component (`hello-world`)
2. User interaction (input updates)
3. Property-based rendering
4. Component composition (`hello-dashboard`)
Your **TestDrive-UI** foundation now supports:
* Isolated unit tests (`Mocha + jsdom`)
* Composed component testing
* Interactive browser previews (`Vite`)
---
Continue to **Event communication** between components — e.g., the dashboard listening to events fired by each greeter when they change their name.
xxx

View File

@@ -1,252 +1,252 @@
# Tutorial 5: Component Communication
Welcome to the **fifth tutorial** takes you into one of the most important patterns in UI development: **communication between components.**
Up to now, your `<hello-world>` components have been independent. In this tutorial, youll make them *talk* to their parent `<hello-dashboard>` component via **custom events**, keeping everything under **TDD**.
---
# 🧭 Tutorial 5 — Component Communication
### “Talking Components” — events between parent and child
---
## 🎯 Goal
Well make each `<hello-world>` component **emit an event** when its name changes.
The `<hello-dashboard>` component will **listen to these events** and maintain a live **activity log**.
The flow will look like this:
```
[User types in <hello-world> input]
<hello-world> fires event "name-changed"
<hello-dashboard> listens, updates log
Log list displays latest updates
```
---
## 🧪 1. Step 1 — Write the failing test first
Create `src/components/hello-dashboard/hello-dashboard.events.test.js`:
```javascript
import "../hello-world/hello-world.js";
import "./hello-dashboard.js";
describe("<hello-dashboard> (events)", () => {
it("shows a log section that updates when a child emits 'name-changed'", async () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
// Initial render
let log = el.shadowRoot.querySelector(".log");
expect(log.textContent).to.include("No activity yet");
// Simulate child event
const firstChild = el.shadowRoot.querySelector("hello-world");
const event = new CustomEvent("name-changed", {
detail: { oldName: "World", newName: "Agent" },
bubbles: true,
composed: true
});
firstChild.dispatchEvent(event);
await el.updateComplete;
log = el.shadowRoot.querySelector(".log");
expect(log.textContent).to.include("World → Agent");
});
});
```
Run:
```bash
npm test
```
The test fails (expected).
---
## 🧩 2. Step 2 — Update `<hello-world>` to fire events
Open `src/components/hello-world/hello-world.js`
and update `_onInput()` to emit an event whenever the name changes:
```javascript
_onInput(event) {
const oldName = this.name;
this.name = event.target.value;
this.dispatchEvent(
new CustomEvent("name-changed", {
detail: { oldName, newName: this.name },
bubbles: true,
composed: true
})
);
}
```
Thats it — now `hello-world` tells the world when its name changes.
---
## ⚙️ 3. Step 3 — Update `<hello-dashboard>` to handle events
Edit `src/components/hello-dashboard/hello-dashboard.js`:
Add a new property and event handler:
```javascript
constructor() {
super();
this.greeters = ["World"];
this.logs = [];
}
connectedCallback() {
super.connectedCallback();
this.addEventListener("name-changed", this._onNameChanged);
}
_onNameChanged = (e) => {
const { oldName, newName } = e.detail;
this.logs = [`${oldName}${newName}`, ...this.logs];
};
```
Now render the log area under the button:
```javascript
render() {
return html`
<div class="container">
<h2>Hello Dashboard</h2>
<div class="counter">Total greetings: ${this.greeters.length}</div>
${this.greeters.map(
(name) => html`<hello-world name=${name}></hello-world>`
)}
<button @click=${this._addGreeter}>Add Greeting</button>
<div class="log">
${this.logs.length === 0
? html`<p>No activity yet</p>`
: html`<ul>
${this.logs.map((l) => html`<li>${l}</li>`)}
</ul>`}
</div>
</div>
`;
}
```
Run tests again:
```bash
npm test
```
✅ The event test should now pass.
---
## 🌐 4. Step 4 — Try it live
In `src/index.html`:
```html
<hello-dashboard></hello-dashboard>
```
Then:
```bash
npm run dev
```
Open the page —
Type into a greeter input, and youll see a live **activity log** appear below the button:
```
Hello Dashboard
Total greetings: 1
Hello, World!
[ Add Greeting ]
Activity Log:
World → Agent
```
---
## 🧠 5. Step 5 — Add a test for multiple events (optional)
Extend `hello-dashboard.events.test.js`:
```javascript
it("keeps a running list of multiple name changes", async () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
const child = el.shadowRoot.querySelector("hello-world");
const events = [
new CustomEvent("name-changed", { detail: { oldName: "World", newName: "A" }, bubbles: true, composed: true }),
new CustomEvent("name-changed", { detail: { oldName: "A", newName: "B" }, bubbles: true, composed: true })
];
child.dispatchEvent(events[0]);
child.dispatchEvent(events[1]);
await el.updateComplete;
const items = Array.from(el.shadowRoot.querySelectorAll(".log li")).map(li => li.textContent);
expect(items).to.deep.equal(["A → B", "World → A"]);
});
```
✅ It should pass too.
---
## 🔩 6. Key Concepts
| Concept | Explanation |
| -------------------------- | ----------------------------------------------------------------- |
| **CustomEvent** | A way for child components to send structured messages to parents |
| **Bubbling + composed** | Ensures events cross shadow DOM boundaries |
| **Reactive logs** | Updating an array property triggers rerender |
| **Hierarchical testing** | TDD across parent/child relationships |
| **Single source of truth** | Dashboard owns state; children only emit events |
---
## ✅ 7. Summary
Youve now built:
1. Independent reactive components (`hello-world`)
2. Composed layouts (`hello-dashboard`)
3. Two-way UI interaction (input updates)
4. **Inter-component event communication**
Your **TestDrive-UI** framework now supports:
* Pure front-end logic
* Reactive re-rendering
* Nested component tests
* Event-driven state management
---
Next step (Tutorial 6) will cover:
> **State synchronization** — introducing a shared store or context (e.g. using Lits reactive controllers or a lightweight signal system) so that multiple components reflect shared data automatically.
Would you like to continue with that direction next?
# Tutorial 5: Component Communication
Welcome to the **fifth tutorial** takes you into one of the most important patterns in UI development: **communication between components.**
Up to now, your `<hello-world>` components have been independent. In this tutorial, youll make them *talk* to their parent `<hello-dashboard>` component via **custom events**, keeping everything under **TDD**.
---
# 🧭 Tutorial 5 — Component Communication
### “Talking Components” — events between parent and child
---
## 🎯 Goal
Well make each `<hello-world>` component **emit an event** when its name changes.
The `<hello-dashboard>` component will **listen to these events** and maintain a live **activity log**.
The flow will look like this:
```
[User types in <hello-world> input]
<hello-world> fires event "name-changed"
<hello-dashboard> listens, updates log
Log list displays latest updates
```
---
## 🧪 1. Step 1 — Write the failing test first
Create `src/components/hello-dashboard/hello-dashboard.events.test.js`:
```javascript
import "../hello-world/hello-world.js";
import "./hello-dashboard.js";
describe("<hello-dashboard> (events)", () => {
it("shows a log section that updates when a child emits 'name-changed'", async () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
// Initial render
let log = el.shadowRoot.querySelector(".log");
expect(log.textContent).to.include("No activity yet");
// Simulate child event
const firstChild = el.shadowRoot.querySelector("hello-world");
const event = new CustomEvent("name-changed", {
detail: { oldName: "World", newName: "Agent" },
bubbles: true,
composed: true
});
firstChild.dispatchEvent(event);
await el.updateComplete;
log = el.shadowRoot.querySelector(".log");
expect(log.textContent).to.include("World → Agent");
});
});
```
Run:
```bash
npm test
```
The test fails (expected).
---
## 🧩 2. Step 2 — Update `<hello-world>` to fire events
Open `src/components/hello-world/hello-world.js`
and update `_onInput()` to emit an event whenever the name changes:
```javascript
_onInput(event) {
const oldName = this.name;
this.name = event.target.value;
this.dispatchEvent(
new CustomEvent("name-changed", {
detail: { oldName, newName: this.name },
bubbles: true,
composed: true
})
);
}
```
Thats it — now `hello-world` tells the world when its name changes.
---
## ⚙️ 3. Step 3 — Update `<hello-dashboard>` to handle events
Edit `src/components/hello-dashboard/hello-dashboard.js`:
Add a new property and event handler:
```javascript
constructor() {
super();
this.greeters = ["World"];
this.logs = [];
}
connectedCallback() {
super.connectedCallback();
this.addEventListener("name-changed", this._onNameChanged);
}
_onNameChanged = (e) => {
const { oldName, newName } = e.detail;
this.logs = [`${oldName}${newName}`, ...this.logs];
};
```
Now render the log area under the button:
```javascript
render() {
return html`
<div class="container">
<h2>Hello Dashboard</h2>
<div class="counter">Total greetings: ${this.greeters.length}</div>
${this.greeters.map(
(name) => html`<hello-world name=${name}></hello-world>`
)}
<button @click=${this._addGreeter}>Add Greeting</button>
<div class="log">
${this.logs.length === 0
? html`<p>No activity yet</p>`
: html`<ul>
${this.logs.map((l) => html`<li>${l}</li>`)}
</ul>`}
</div>
</div>
`;
}
```
Run tests again:
```bash
npm test
```
✅ The event test should now pass.
---
## 🌐 4. Step 4 — Try it live
In `src/index.html`:
```html
<hello-dashboard></hello-dashboard>
```
Then:
```bash
npm run dev
```
Open the page —
Type into a greeter input, and youll see a live **activity log** appear below the button:
```
Hello Dashboard
Total greetings: 1
Hello, World!
[ Add Greeting ]
Activity Log:
World → Agent
```
---
## 🧠 5. Step 5 — Add a test for multiple events (optional)
Extend `hello-dashboard.events.test.js`:
```javascript
it("keeps a running list of multiple name changes", async () => {
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
const child = el.shadowRoot.querySelector("hello-world");
const events = [
new CustomEvent("name-changed", { detail: { oldName: "World", newName: "A" }, bubbles: true, composed: true }),
new CustomEvent("name-changed", { detail: { oldName: "A", newName: "B" }, bubbles: true, composed: true })
];
child.dispatchEvent(events[0]);
child.dispatchEvent(events[1]);
await el.updateComplete;
const items = Array.from(el.shadowRoot.querySelectorAll(".log li")).map(li => li.textContent);
expect(items).to.deep.equal(["A → B", "World → A"]);
});
```
✅ It should pass too.
---
## 🔩 6. Key Concepts
| Concept | Explanation |
| -------------------------- | ----------------------------------------------------------------- |
| **CustomEvent** | A way for child components to send structured messages to parents |
| **Bubbling + composed** | Ensures events cross shadow DOM boundaries |
| **Reactive logs** | Updating an array property triggers rerender |
| **Hierarchical testing** | TDD across parent/child relationships |
| **Single source of truth** | Dashboard owns state; children only emit events |
---
## ✅ 7. Summary
Youve now built:
1. Independent reactive components (`hello-world`)
2. Composed layouts (`hello-dashboard`)
3. Two-way UI interaction (input updates)
4. **Inter-component event communication**
Your **TestDrive-UI** framework now supports:
* Pure front-end logic
* Reactive re-rendering
* Nested component tests
* Event-driven state management
---
Next step (Tutorial 6) will cover:
> **State synchronization** — introducing a shared store or context (e.g. using Lits reactive controllers or a lightweight signal system) so that multiple components reflect shared data automatically.
Would you like to continue with that direction next?
xxx

View File

@@ -1,322 +1,322 @@
# Tutorial 6: Shared State
Now that your components can communicate through events, the **sixth tutorial** introduces the next natural step in professional UI architecture: **shared state** (synchronizing multiple components via a central store).
Youll learn how to implement a **lightweight reactive store** with Lit, integrate it with your existing `<hello-world>` and `<hello-dashboard>` components, and keep everything **test-driven**.
---
# ⚡ Tutorial 6 — Shared State
### “The Store Pattern” — Synchronizing Multiple Components
---
## 🎯 Goal
Well create a **store** that holds the global list of greeter names.
All `<hello-world>` components will automatically update when the store changes.
The `<hello-dashboard>` will use the store to add, remove, and rename greeters — without manually passing props or listening to custom events.
**Architecture overview:**
```
+------------------------+
| HelloDashboard |
| (manages store ops) |
| |
| ┌────────────┐ |
| | HelloWorld | <── store.subscribe()
| └────────────┘ |
| ┌────────────┐ |
| | HelloWorld | |
| └────────────┘ |
+------------------------+
Shared Store
(holds array of greeters)
```
---
## 🧪 1. Step 1 — Write the failing test first
Create `src/store/hello-store.test.js`:
```javascript
import { helloStore } from "./hello-store.js";
describe("helloStore", () => {
it("starts with one default greeter", () => {
const state = helloStore.getState();
expect(state.greeters).to.deep.equal(["World"]);
});
it("can add a new greeter", () => {
helloStore.addGreeter("Alice");
const state = helloStore.getState();
expect(state.greeters).to.include("Alice");
});
it("notifies subscribers on change", (done) => {
const unsubscribe = helloStore.subscribe((state) => {
expect(state.greeters).to.include("Bob");
unsubscribe();
done();
});
helloStore.addGreeter("Bob");
});
});
```
Run:
```bash
npm test
```
All tests fail (expected).
---
## ⚙️ 2. Step 2 — Implement the store
Create a new file:
`src/store/hello-store.js`
```javascript
class HelloStore {
constructor() {
this.state = { greeters: ["World"] };
this.listeners = new Set();
}
getState() {
return this.state;
}
_notify() {
for (const cb of this.listeners) cb(this.state);
}
subscribe(callback) {
this.listeners.add(callback);
callback(this.state); // immediate call with current state
return () => this.listeners.delete(callback);
}
addGreeter(name) {
this.state = {
...this.state,
greeters: [...this.state.greeters, name]
};
this._notify();
}
renameGreeter(oldName, newName) {
const updated = this.state.greeters.map((n) =>
n === oldName ? newName : n
);
this.state = { ...this.state, greeters: updated };
this._notify();
}
}
export const helloStore = new HelloStore();
```
Re-run:
```bash
npm test
```
✅ All tests should now pass.
---
## 🧩 3. Step 3 — Update `<hello-dashboard>` to use the store
Modify `src/components/hello-dashboard/hello-dashboard.js`:
```javascript
import { LitElement, html, css } from "lit";
import "../hello-world/hello-world.js";
import { helloStore } from "../../store/hello-store.js";
export class HelloDashboard extends LitElement {
static styles = css`
.container {
font-family: system-ui, sans-serif;
padding: 1rem;
text-align: center;
}
.counter {
margin: 0.5rem 0 1rem 0;
font-weight: bold;
color: #007acc;
}
button {
background: #007acc;
color: white;
border: none;
border-radius: 6px;
padding: 0.4rem 1rem;
cursor: pointer;
}
`;
constructor() {
super();
this.greeters = [];
}
connectedCallback() {
super.connectedCallback();
this.unsubscribe = helloStore.subscribe((state) => {
this.greeters = state.greeters;
});
}
disconnectedCallback() {
this.unsubscribe?.();
super.disconnectedCallback();
}
_addGreeter() {
const newName = `User${this.greeters.length + 1}`;
helloStore.addGreeter(newName);
}
render() {
return html`
<div class="container">
<h2>Hello Dashboard</h2>
<div class="counter">Total greetings: ${this.greeters.length}</div>
${this.greeters.map(
(name) => html`<hello-world name=${name}></hello-world>`
)}
<button @click=${this._addGreeter}>Add Greeting</button>
</div>
`;
}
}
customElements.define("hello-dashboard", HelloDashboard);
```
---
## 🧠 4. Step 4 — Update `<hello-world>` to use store for renaming
Update `_onInput()` in `src/components/hello-world/hello-world.js`:
```javascript
import { helloStore } from "../../store/hello-store.js";
_onInput(event) {
const oldName = this.name;
this.name = event.target.value;
helloStore.renameGreeter(oldName, this.name);
}
```
Now each greeter updates the shared store when its input changes — all other components subscribed to the store will react automatically.
---
## 🧪 5. Step 5 — Integration test (optional)
Add `src/integration/dashboard-store.test.js`:
```javascript
import "../components/hello-dashboard/hello-dashboard.js";
import { helloStore } from "../store/hello-store.js";
describe("HelloDashboard and HelloStore integration", () => {
it("renders as many greeters as store entries", async () => {
helloStore.addGreeter("Eve");
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
await el.updateComplete;
const greeters = el.shadowRoot.querySelectorAll("hello-world");
expect(greeters.length).to.equal(helloStore.getState().greeters.length);
});
});
```
Run:
```bash
npm test
```
✅ All pass again.
---
## 🌐 6. Step 6 — Try it live
Add to your `index.html`:
```html
<hello-dashboard></hello-dashboard>
```
Run:
```bash
npm run dev
```
* Click “Add Greeting” → new greeters appear instantly.
* Type into any greeter → all components referencing that name update automatically.
Congratulations — youve just implemented **a reactive state system** entirely in vanilla JS + Lit!
---
## 📘 7. Key Concepts
| Concept | Description |
| -------------------------- | ----------------------------------------------------- |
| **Centralized state** | One store manages the data for all components |
| **Observer pattern** | Components subscribe to updates, re-render on change |
| **Reactive sync** | Changes propagate automatically without prop drilling |
| **TDD-first store design** | Ensures predictable, testable state behavior |
| **Agent-friendliness** | Clear separation of state, presentation, and tests |
---
## ✅ 8. Summary
At this point, your **TestDrive-UI** ecosystem includes:
| Layer | Responsibility |
| ----------------- | ---------------------------- |
| `hello-world` | Simple reactive component |
| `hello-dashboard` | Container and coordinator |
| `hello-store` | Shared, observable state |
| Tests | Guard behavior at each level |
---
## 🔮 9. Next Tutorial (optional direction)
You can now evolve in one of two directions:
1. **Tutorial 7 — Persistence**
→ Save and restore store state via `localStorage` or IndexedDB (so greetings persist).
2. **Tutorial 8 — Agent Automation**
→ Introduce agent-driven TDD loops: use LLMs to generate new tests, detect regressions, and propose refactorings automatically.
---
Continue with **Tutorial 7: State Persistence** next — or move directly into **agent-driven TDD automation**.
xxx
# Tutorial 6: Shared State
Now that your components can communicate through events, the **sixth tutorial** introduces the next natural step in professional UI architecture: **shared state** (synchronizing multiple components via a central store).
Youll learn how to implement a **lightweight reactive store** with Lit, integrate it with your existing `<hello-world>` and `<hello-dashboard>` components, and keep everything **test-driven**.
---
# ⚡ Tutorial 6 — Shared State
### “The Store Pattern” — Synchronizing Multiple Components
---
## 🎯 Goal
Well create a **store** that holds the global list of greeter names.
All `<hello-world>` components will automatically update when the store changes.
The `<hello-dashboard>` will use the store to add, remove, and rename greeters — without manually passing props or listening to custom events.
**Architecture overview:**
```
+------------------------+
| HelloDashboard |
| (manages store ops) |
| |
| ┌────────────┐ |
| | HelloWorld | <── store.subscribe()
| └────────────┘ |
| ┌────────────┐ |
| | HelloWorld | |
| └────────────┘ |
+------------------------+
Shared Store
(holds array of greeters)
```
---
## 🧪 1. Step 1 — Write the failing test first
Create `src/store/hello-store.test.js`:
```javascript
import { helloStore } from "./hello-store.js";
describe("helloStore", () => {
it("starts with one default greeter", () => {
const state = helloStore.getState();
expect(state.greeters).to.deep.equal(["World"]);
});
it("can add a new greeter", () => {
helloStore.addGreeter("Alice");
const state = helloStore.getState();
expect(state.greeters).to.include("Alice");
});
it("notifies subscribers on change", (done) => {
const unsubscribe = helloStore.subscribe((state) => {
expect(state.greeters).to.include("Bob");
unsubscribe();
done();
});
helloStore.addGreeter("Bob");
});
});
```
Run:
```bash
npm test
```
All tests fail (expected).
---
## ⚙️ 2. Step 2 — Implement the store
Create a new file:
`src/store/hello-store.js`
```javascript
class HelloStore {
constructor() {
this.state = { greeters: ["World"] };
this.listeners = new Set();
}
getState() {
return this.state;
}
_notify() {
for (const cb of this.listeners) cb(this.state);
}
subscribe(callback) {
this.listeners.add(callback);
callback(this.state); // immediate call with current state
return () => this.listeners.delete(callback);
}
addGreeter(name) {
this.state = {
...this.state,
greeters: [...this.state.greeters, name]
};
this._notify();
}
renameGreeter(oldName, newName) {
const updated = this.state.greeters.map((n) =>
n === oldName ? newName : n
);
this.state = { ...this.state, greeters: updated };
this._notify();
}
}
export const helloStore = new HelloStore();
```
Re-run:
```bash
npm test
```
✅ All tests should now pass.
---
## 🧩 3. Step 3 — Update `<hello-dashboard>` to use the store
Modify `src/components/hello-dashboard/hello-dashboard.js`:
```javascript
import { LitElement, html, css } from "lit";
import "../hello-world/hello-world.js";
import { helloStore } from "../../store/hello-store.js";
export class HelloDashboard extends LitElement {
static styles = css`
.container {
font-family: system-ui, sans-serif;
padding: 1rem;
text-align: center;
}
.counter {
margin: 0.5rem 0 1rem 0;
font-weight: bold;
color: #007acc;
}
button {
background: #007acc;
color: white;
border: none;
border-radius: 6px;
padding: 0.4rem 1rem;
cursor: pointer;
}
`;
constructor() {
super();
this.greeters = [];
}
connectedCallback() {
super.connectedCallback();
this.unsubscribe = helloStore.subscribe((state) => {
this.greeters = state.greeters;
});
}
disconnectedCallback() {
this.unsubscribe?.();
super.disconnectedCallback();
}
_addGreeter() {
const newName = `User${this.greeters.length + 1}`;
helloStore.addGreeter(newName);
}
render() {
return html`
<div class="container">
<h2>Hello Dashboard</h2>
<div class="counter">Total greetings: ${this.greeters.length}</div>
${this.greeters.map(
(name) => html`<hello-world name=${name}></hello-world>`
)}
<button @click=${this._addGreeter}>Add Greeting</button>
</div>
`;
}
}
customElements.define("hello-dashboard", HelloDashboard);
```
---
## 🧠 4. Step 4 — Update `<hello-world>` to use store for renaming
Update `_onInput()` in `src/components/hello-world/hello-world.js`:
```javascript
import { helloStore } from "../../store/hello-store.js";
_onInput(event) {
const oldName = this.name;
this.name = event.target.value;
helloStore.renameGreeter(oldName, this.name);
}
```
Now each greeter updates the shared store when its input changes — all other components subscribed to the store will react automatically.
---
## 🧪 5. Step 5 — Integration test (optional)
Add `src/integration/dashboard-store.test.js`:
```javascript
import "../components/hello-dashboard/hello-dashboard.js";
import { helloStore } from "../store/hello-store.js";
describe("HelloDashboard and HelloStore integration", () => {
it("renders as many greeters as store entries", async () => {
helloStore.addGreeter("Eve");
const el = document.createElement("hello-dashboard");
document.body.appendChild(el);
await el.updateComplete;
const greeters = el.shadowRoot.querySelectorAll("hello-world");
expect(greeters.length).to.equal(helloStore.getState().greeters.length);
});
});
```
Run:
```bash
npm test
```
✅ All pass again.
---
## 🌐 6. Step 6 — Try it live
Add to your `index.html`:
```html
<hello-dashboard></hello-dashboard>
```
Run:
```bash
npm run dev
```
* Click “Add Greeting” → new greeters appear instantly.
* Type into any greeter → all components referencing that name update automatically.
Congratulations — youve just implemented **a reactive state system** entirely in vanilla JS + Lit!
---
## 📘 7. Key Concepts
| Concept | Description |
| -------------------------- | ----------------------------------------------------- |
| **Centralized state** | One store manages the data for all components |
| **Observer pattern** | Components subscribe to updates, re-render on change |
| **Reactive sync** | Changes propagate automatically without prop drilling |
| **TDD-first store design** | Ensures predictable, testable state behavior |
| **Agent-friendliness** | Clear separation of state, presentation, and tests |
---
## ✅ 8. Summary
At this point, your **TestDrive-UI** ecosystem includes:
| Layer | Responsibility |
| ----------------- | ---------------------------- |
| `hello-world` | Simple reactive component |
| `hello-dashboard` | Container and coordinator |
| `hello-store` | Shared, observable state |
| Tests | Guard behavior at each level |
---
## 🔮 9. Next Tutorial (optional direction)
You can now evolve in one of two directions:
1. **Tutorial 7 — Persistence**
→ Save and restore store state via `localStorage` or IndexedDB (so greetings persist).
2. **Tutorial 8 — Agent Automation**
→ Introduce agent-driven TDD loops: use LLMs to generate new tests, detect regressions, and propose refactorings automatically.
---
Continue with **Tutorial 7: State Persistence** next — or move directly into **agent-driven TDD automation**.
xxx

View File

@@ -1,238 +1,238 @@
# Tutorial 7: Persistence Patterns
This tutorial teaches both **localStorage** and **IndexedDB** persistence patterns for your reactive store.
### “Saving Greetings” — Making the store survive reloads
---
## 🎯 Goal
Extend the `helloStore` so that all greetings persist between browser sessions.
When the page reloads, previously added greeters are restored automatically.
Well implement this in **two steps**:
1. Start with simple **`localStorage`** persistence (fast, synchronous).
2. Upgrade to **`IndexedDB`** for larger data or structured offline storage.
Both variants will keep your TestDrive-UI environment self-contained and testable.
---
## 🧪 Step 1 — Write the failing tests
Create a new file:
`src/store/hello-store.persistence.test.js`
```javascript
import { helloStore } from "./hello-store.js";
describe("helloStore persistence", () => {
beforeEach(() => {
// Clean simulated storage
global.localStorage = {
store: {},
getItem(k) { return this.store[k] || null; },
setItem(k, v) { this.store[k] = v; },
clear() { this.store = {}; }
};
});
it("saves state to localStorage after modification", () => {
helloStore.addGreeter("Persisty");
const saved = JSON.parse(localStorage.getItem("helloStore"));
expect(saved.greeters).to.include("Persisty");
});
it("restores saved state when re-initialized", () => {
localStorage.setItem(
"helloStore",
JSON.stringify({ greeters: ["Restored"] })
);
const { HelloStore } = await import("./hello-store.js");
const freshStore = new HelloStore();
const state = freshStore.getState();
expect(state.greeters).to.deep.equal(["Restored"]);
});
});
```
Run:
```bash
npm test
```
All tests will fail initially — perfect.
---
## ⚙️ Step 2 — Implement `localStorage` persistence
Open `src/store/hello-store.js` and extend it like this:
```javascript
class HelloStore {
constructor() {
const saved = localStorage.getItem("helloStore");
this.state = saved ? JSON.parse(saved) : { greeters: ["World"] };
this.listeners = new Set();
}
getState() {
return this.state;
}
_notify() {
// Save current state before notifying
localStorage.setItem("helloStore", JSON.stringify(this.state));
for (const cb of this.listeners) cb(this.state);
}
subscribe(callback) {
this.listeners.add(callback);
callback(this.state);
return () => this.listeners.delete(callback);
}
addGreeter(name) {
this.state = { ...this.state, greeters: [...this.state.greeters, name] };
this._notify();
}
renameGreeter(oldName, newName) {
const updated = this.state.greeters.map((n) =>
n === oldName ? newName : n
);
this.state = { ...this.state, greeters: updated };
this._notify();
}
}
export const helloStore = new HelloStore();
export { HelloStore }; // exported for testing
```
✅ Run `npm test` again — tests should now pass.
---
## 🌐 Step 3 — Try it live
Start your dev server:
```bash
npm run dev
```
1. Add new greeters in the dashboard.
2. Reload the browser.
→ The same greeters reappear immediately!
---
## 🧱 Step 4 — Optional: Switch to IndexedDB for larger data
`localStorage` is fine for small JSON objects.
To persist more complex data or avoid blocking the main thread, you can use **IndexedDB** (via the tiny `idb-keyval` helper or native API).
Create `src/store/storage.js`:
```javascript
// Minimal IndexedDB wrapper using idb-keyval style API
const DB_NAME = "testdrive-ui";
const STORE_NAME = "hello";
export async function saveState(state) {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME);
req.onsuccess = () => {
const tx = req.result.transaction(STORE_NAME, "readwrite");
tx.objectStore(STORE_NAME).put(state, "store");
tx.oncomplete = resolve;
tx.onerror = reject;
};
});
}
export async function loadState() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME);
req.onsuccess = () => {
const tx = req.result.transaction(STORE_NAME, "readonly");
const get = tx.objectStore(STORE_NAME).get("store");
get.onsuccess = () => resolve(get.result);
get.onerror = reject;
};
});
}
```
Now update the stores `_notify()` and constructor to use these async calls:
```javascript
import { saveState, loadState } from "./storage.js";
class HelloStore {
constructor() {
this.listeners = new Set();
loadState().then(
(saved) =>
(this.state = saved || { greeters: ["World"] }) &&
this._notify()
);
}
async _notify() {
await saveState(this.state);
for (const cb of this.listeners) cb(this.state);
}
}
```
💡 Tip: because IndexedDB operations are async, wrap state changes in `async` methods and adjust your tests with `await`.
---
## 🧠 Step 5 — Testing IndexedDB logic
In Node environments, you can simulate IndexedDB with `fake-indexeddb`:
```bash
npm install --save-dev fake-indexeddb
```
Then in your test setup:
```javascript
import "fake-indexeddb/auto";
```
Your tests now work without a browser.
---
## ✅ Outcome
Your TestDrive-UI environment now supports:
| Persistence Layer | Use Case | Pros |
| ----------------- | ------------------------------------- | ------------------ |
| **localStorage** | Small, synchronous, quick persistence | Simple & immediate |
| **IndexedDB** | Large, structured, async persistence | Scalable & robust |
---
## 🔍 Reflection
Persistence transforms your store into a **long-term memory layer**.
With this, TestDrive-UI crosses into **progressive-web-app territory** — offline-ready, recoverable, and reliable.
---
Continue to Tutorial 8 on Agent Automation.
xxx
# Tutorial 7: Persistence Patterns
This tutorial teaches both **localStorage** and **IndexedDB** persistence patterns for your reactive store.
### “Saving Greetings” — Making the store survive reloads
---
## 🎯 Goal
Extend the `helloStore` so that all greetings persist between browser sessions.
When the page reloads, previously added greeters are restored automatically.
Well implement this in **two steps**:
1. Start with simple **`localStorage`** persistence (fast, synchronous).
2. Upgrade to **`IndexedDB`** for larger data or structured offline storage.
Both variants will keep your TestDrive-UI environment self-contained and testable.
---
## 🧪 Step 1 — Write the failing tests
Create a new file:
`src/store/hello-store.persistence.test.js`
```javascript
import { helloStore } from "./hello-store.js";
describe("helloStore persistence", () => {
beforeEach(() => {
// Clean simulated storage
global.localStorage = {
store: {},
getItem(k) { return this.store[k] || null; },
setItem(k, v) { this.store[k] = v; },
clear() { this.store = {}; }
};
});
it("saves state to localStorage after modification", () => {
helloStore.addGreeter("Persisty");
const saved = JSON.parse(localStorage.getItem("helloStore"));
expect(saved.greeters).to.include("Persisty");
});
it("restores saved state when re-initialized", () => {
localStorage.setItem(
"helloStore",
JSON.stringify({ greeters: ["Restored"] })
);
const { HelloStore } = await import("./hello-store.js");
const freshStore = new HelloStore();
const state = freshStore.getState();
expect(state.greeters).to.deep.equal(["Restored"]);
});
});
```
Run:
```bash
npm test
```
All tests will fail initially — perfect.
---
## ⚙️ Step 2 — Implement `localStorage` persistence
Open `src/store/hello-store.js` and extend it like this:
```javascript
class HelloStore {
constructor() {
const saved = localStorage.getItem("helloStore");
this.state = saved ? JSON.parse(saved) : { greeters: ["World"] };
this.listeners = new Set();
}
getState() {
return this.state;
}
_notify() {
// Save current state before notifying
localStorage.setItem("helloStore", JSON.stringify(this.state));
for (const cb of this.listeners) cb(this.state);
}
subscribe(callback) {
this.listeners.add(callback);
callback(this.state);
return () => this.listeners.delete(callback);
}
addGreeter(name) {
this.state = { ...this.state, greeters: [...this.state.greeters, name] };
this._notify();
}
renameGreeter(oldName, newName) {
const updated = this.state.greeters.map((n) =>
n === oldName ? newName : n
);
this.state = { ...this.state, greeters: updated };
this._notify();
}
}
export const helloStore = new HelloStore();
export { HelloStore }; // exported for testing
```
✅ Run `npm test` again — tests should now pass.
---
## 🌐 Step 3 — Try it live
Start your dev server:
```bash
npm run dev
```
1. Add new greeters in the dashboard.
2. Reload the browser.
→ The same greeters reappear immediately!
---
## 🧱 Step 4 — Optional: Switch to IndexedDB for larger data
`localStorage` is fine for small JSON objects.
To persist more complex data or avoid blocking the main thread, you can use **IndexedDB** (via the tiny `idb-keyval` helper or native API).
Create `src/store/storage.js`:
```javascript
// Minimal IndexedDB wrapper using idb-keyval style API
const DB_NAME = "testdrive-ui";
const STORE_NAME = "hello";
export async function saveState(state) {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME);
req.onsuccess = () => {
const tx = req.result.transaction(STORE_NAME, "readwrite");
tx.objectStore(STORE_NAME).put(state, "store");
tx.oncomplete = resolve;
tx.onerror = reject;
};
});
}
export async function loadState() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME);
req.onsuccess = () => {
const tx = req.result.transaction(STORE_NAME, "readonly");
const get = tx.objectStore(STORE_NAME).get("store");
get.onsuccess = () => resolve(get.result);
get.onerror = reject;
};
});
}
```
Now update the stores `_notify()` and constructor to use these async calls:
```javascript
import { saveState, loadState } from "./storage.js";
class HelloStore {
constructor() {
this.listeners = new Set();
loadState().then(
(saved) =>
(this.state = saved || { greeters: ["World"] }) &&
this._notify()
);
}
async _notify() {
await saveState(this.state);
for (const cb of this.listeners) cb(this.state);
}
}
```
💡 Tip: because IndexedDB operations are async, wrap state changes in `async` methods and adjust your tests with `await`.
---
## 🧠 Step 5 — Testing IndexedDB logic
In Node environments, you can simulate IndexedDB with `fake-indexeddb`:
```bash
npm install --save-dev fake-indexeddb
```
Then in your test setup:
```javascript
import "fake-indexeddb/auto";
```
Your tests now work without a browser.
---
## ✅ Outcome
Your TestDrive-UI environment now supports:
| Persistence Layer | Use Case | Pros |
| ----------------- | ------------------------------------- | ------------------ |
| **localStorage** | Small, synchronous, quick persistence | Simple & immediate |
| **IndexedDB** | Large, structured, async persistence | Scalable & robust |
---
## 🔍 Reflection
Persistence transforms your store into a **long-term memory layer**.
With this, TestDrive-UI crosses into **progressive-web-app territory** — offline-ready, recoverable, and reliable.
---
Continue to Tutorial 8 on Agent Automation.
xxx

View File

@@ -1,242 +1,242 @@
# 🤖 Tutorial 8 — Agent-Driven TDD Automation
This tutorial focuses on *Agent-Driven TDD Automation* — turning TestDrive-UI into a living collaboration space between human developers and coding agents.
### “Coding with Companions” — Using AI Agents for Continuous Improvement
---
## 🎯 Goal
Integrate LLM-based coding agents into your TestDrive-UI workflow so they can:
1. **Generate new tests** from natural-language requirements.
2. **Run tests and detect regressions** automatically.
3. **Propose targeted refactorings or patches** when failures occur.
By combining deterministic testing with creative reasoning, you build a feedback loop that never stops improving your code.
---
## 🧩 Concept Overview
Traditional TDD cycle:
```
requirement → test → fail → code → pass → refactor
```
Agent-Driven TDD cycle:
```
requirement → agent generates test → run → fail →
agent proposes fix → run → pass → review → merge
```
You remain the architect and reviewer — the agent acts as an automated junior developer executing fast, repeatable loops.
---
## 🧪 Step 1 — Define a Machine-Readable Requirement
Create a simple JSON file to store behavioral specifications.
`specs/hello-world.name-change.json`
```json
{
"component": "hello-world",
"feature": "name-change",
"description": "Greeting text should update when the user types a new name.",
"expected_behavior": [
"Typing in the input field updates the displayed greeting immediately.",
"The component property 'name' reflects the latest input value."
]
}
```
Agents use these files as prompts to generate corresponding tests.
---
## ⚙️ Step 2 — Agent Generates a Test
An agent reads the JSON spec and produces Mocha tests automatically.
Example output from an LLM agent:
`generated-tests/hello-world.name-change.test.js`
```javascript
import "./hello-world.js";
describe("<hello-world> (auto-generated name-change)", () => {
it("updates greeting text when user types", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const input = el.shadowRoot.querySelector("input");
input.value = "Agent";
input.dispatchEvent(new Event("input"));
await el.updateComplete;
const greeting = el.shadowRoot.querySelector(".greeting");
expect(greeting.textContent.trim()).to.equal("Hello, Agent!");
});
});
```
Run your full suite:
```bash
npm test
```
If the test fails, the agent analyzes output and proposes a minimal fix for `hello-world.js`.
---
## 🔍 Step 3 — Detect Regressions
Agents monitor test results over time by parsing Mochas machine-readable report (`--reporter json`).
Example agent logic (pseudocode):
```python
def analyze_report(report):
failed = [t for t in report['tests'] if t['err']]
if not failed:
return "All green!"
for f in failed:
print(f"Regression in {f['title']}: {f['err']['message']}")
propose_fix(f)
```
This analysis lets agents flag new failures, auto-create issues, or propose PRs.
---
## 🧠 Step 4 — Agent Proposes Refactorings
Once tests are green, agents can examine code for quality signals:
| Heuristic | Example Action |
| --------------------- | ------------------------------------------ |
| Duplicate logic | Extract shared helper or controller |
| Excessive DOM queries | Cache references or use ReactiveController |
| Long methods | Suggest method splitting |
| Repeated strings | Propose localization constants |
Example prompt to your agent:
```
“Review src/components/hello-world.js and propose a refactor
that reduces duplication without breaking current tests.”
```
The agent runs tests after each change to validate its proposal.
---
## 🔁 Step 5 — Automate the Loop
Create a simple Node script to chain the whole process.
`scripts/agent-tdd.js`
```javascript
import { execSync } from "child_process";
import fs from "fs";
import OpenAI from "openai"; // or another LLM SDK
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
function runTests() {
try {
const output = execSync("npx mocha --reporter json", { encoding: "utf-8" });
return JSON.parse(output);
} catch (e) {
return JSON.parse(e.stdout);
}
}
async function main() {
const report = runTests();
const failed = report.tests.filter(t => t.err.message);
if (failed.length === 0) return console.log("✅ All tests passed");
const prompt = `Some tests failed:\n${JSON.stringify(failed, null, 2)}\n
Propose minimal code changes to fix them.`;
const completion = await client.responses.create({ model: "gpt-5", input: prompt });
fs.writeFileSync("agent-proposal.txt", completion.output_text);
console.log("💡 Agent proposal written to agent-proposal.txt");
}
main();
```
This script connects your tests with an AI assistant that learns and suggests fixes continuously.
---
## 🧰 Step 6 — Integrate into CI
Add a CI job (e.g., GitHub Actions or local cron) to run the agent loop daily or on push.
Example workflow:
```
on:
push:
schedule:
- cron: '0 2 * * *'
jobs:
agent-tdd:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: node scripts/agent-tdd.js
```
Now your project self-tests and self-critiques even when youre offline.
---
## 🧩 Step 7 — Visualize Agent Progress
Agents can log progress into a dashboard component (`<agent-console>`) showing:
* Number of tests generated.
* Pass/fail trend over time.
* Proposed vs. accepted refactors.
Its your window into the machines learning curve.
---
## ✅ Outcome
You now have a self-reinforcing loop:
1. Humans write specs.
2. Agents create tests and code.
3. The suite proves stability.
4. Agents refactor and review under guard of tests.
This combines the discipline of TDD with the creativity and endurance of AI.
---
## 🔍 Next Steps
* Add **semantic diff filters** so agents learn from accepted patches.
* Train agents to cluster tests into *feature domains* for smarter coverage analysis.
* Integrate Storybook snapshots for visual regression detection.
* Build a CLI (`npx agent-tdd`) to run and audit your AI test loops interactively.
---
Congratulations! You finished all tutorials and should be fine going Forward Building components.
Feel free to tell us which additional tutorials we should provide.
# 🤖 Tutorial 8 — Agent-Driven TDD Automation
This tutorial focuses on *Agent-Driven TDD Automation* — turning TestDrive-UI into a living collaboration space between human developers and coding agents.
### “Coding with Companions” — Using AI Agents for Continuous Improvement
---
## 🎯 Goal
Integrate LLM-based coding agents into your TestDrive-UI workflow so they can:
1. **Generate new tests** from natural-language requirements.
2. **Run tests and detect regressions** automatically.
3. **Propose targeted refactorings or patches** when failures occur.
By combining deterministic testing with creative reasoning, you build a feedback loop that never stops improving your code.
---
## 🧩 Concept Overview
Traditional TDD cycle:
```
requirement → test → fail → code → pass → refactor
```
Agent-Driven TDD cycle:
```
requirement → agent generates test → run → fail →
agent proposes fix → run → pass → review → merge
```
You remain the architect and reviewer — the agent acts as an automated junior developer executing fast, repeatable loops.
---
## 🧪 Step 1 — Define a Machine-Readable Requirement
Create a simple JSON file to store behavioral specifications.
`specs/hello-world.name-change.json`
```json
{
"component": "hello-world",
"feature": "name-change",
"description": "Greeting text should update when the user types a new name.",
"expected_behavior": [
"Typing in the input field updates the displayed greeting immediately.",
"The component property 'name' reflects the latest input value."
]
}
```
Agents use these files as prompts to generate corresponding tests.
---
## ⚙️ Step 2 — Agent Generates a Test
An agent reads the JSON spec and produces Mocha tests automatically.
Example output from an LLM agent:
`generated-tests/hello-world.name-change.test.js`
```javascript
import "./hello-world.js";
describe("<hello-world> (auto-generated name-change)", () => {
it("updates greeting text when user types", async () => {
const el = document.createElement("hello-world");
document.body.appendChild(el);
const input = el.shadowRoot.querySelector("input");
input.value = "Agent";
input.dispatchEvent(new Event("input"));
await el.updateComplete;
const greeting = el.shadowRoot.querySelector(".greeting");
expect(greeting.textContent.trim()).to.equal("Hello, Agent!");
});
});
```
Run your full suite:
```bash
npm test
```
If the test fails, the agent analyzes output and proposes a minimal fix for `hello-world.js`.
---
## 🔍 Step 3 — Detect Regressions
Agents monitor test results over time by parsing Mochas machine-readable report (`--reporter json`).
Example agent logic (pseudocode):
```python
def analyze_report(report):
failed = [t for t in report['tests'] if t['err']]
if not failed:
return "All green!"
for f in failed:
print(f"Regression in {f['title']}: {f['err']['message']}")
propose_fix(f)
```
This analysis lets agents flag new failures, auto-create issues, or propose PRs.
---
## 🧠 Step 4 — Agent Proposes Refactorings
Once tests are green, agents can examine code for quality signals:
| Heuristic | Example Action |
| --------------------- | ------------------------------------------ |
| Duplicate logic | Extract shared helper or controller |
| Excessive DOM queries | Cache references or use ReactiveController |
| Long methods | Suggest method splitting |
| Repeated strings | Propose localization constants |
Example prompt to your agent:
```
“Review src/components/hello-world.js and propose a refactor
that reduces duplication without breaking current tests.”
```
The agent runs tests after each change to validate its proposal.
---
## 🔁 Step 5 — Automate the Loop
Create a simple Node script to chain the whole process.
`scripts/agent-tdd.js`
```javascript
import { execSync } from "child_process";
import fs from "fs";
import OpenAI from "openai"; // or another LLM SDK
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
function runTests() {
try {
const output = execSync("npx mocha --reporter json", { encoding: "utf-8" });
return JSON.parse(output);
} catch (e) {
return JSON.parse(e.stdout);
}
}
async function main() {
const report = runTests();
const failed = report.tests.filter(t => t.err.message);
if (failed.length === 0) return console.log("✅ All tests passed");
const prompt = `Some tests failed:\n${JSON.stringify(failed, null, 2)}\n
Propose minimal code changes to fix them.`;
const completion = await client.responses.create({ model: "gpt-5", input: prompt });
fs.writeFileSync("agent-proposal.txt", completion.output_text);
console.log("💡 Agent proposal written to agent-proposal.txt");
}
main();
```
This script connects your tests with an AI assistant that learns and suggests fixes continuously.
---
## 🧰 Step 6 — Integrate into CI
Add a CI job (e.g., GitHub Actions or local cron) to run the agent loop daily or on push.
Example workflow:
```
on:
push:
schedule:
- cron: '0 2 * * *'
jobs:
agent-tdd:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: node scripts/agent-tdd.js
```
Now your project self-tests and self-critiques even when youre offline.
---
## 🧩 Step 7 — Visualize Agent Progress
Agents can log progress into a dashboard component (`<agent-console>`) showing:
* Number of tests generated.
* Pass/fail trend over time.
* Proposed vs. accepted refactors.
Its your window into the machines learning curve.
---
## ✅ Outcome
You now have a self-reinforcing loop:
1. Humans write specs.
2. Agents create tests and code.
3. The suite proves stability.
4. Agents refactor and review under guard of tests.
This combines the discipline of TDD with the creativity and endurance of AI.
---
## 🔍 Next Steps
* Add **semantic diff filters** so agents learn from accepted patches.
* Train agents to cluster tests into *feature domains* for smarter coverage analysis.
* Integrate Storybook snapshots for visual regression detection.
* Build a CLI (`npx agent-tdd`) to run and audit your AI test loops interactively.
---
Congratulations! You finished all tutorials and should be fine going Forward Building components.
Feel free to tell us which additional tutorials we should provide.
xxx