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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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