generated from coulomb/repo-seed
chore: Fixed line endings i tutorials and provided Introduction
This commit is contained in:
199
INTRODUCTION.md
Normal file
199
INTRODUCTION.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# 🧪 Introduction to Test-Driven-UI Development
|
||||
|
||||
Here’s 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 that’s 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 it’s 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** isn’t a framework — it’s a *method*.
|
||||
> It’s how you and your coding agents learn to reason about interfaces through evidence.
|
||||
|
||||
---
|
||||
|
||||
Start with our tutorials to get going.
|
||||
|
||||
xxx
|
||||
@@ -1,144 +1,144 @@
|
||||
List of **next-stage tutorial ideas**, organized by **theme** and **complexity**, to help evolve both the teaching path and the framework.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 **A. UI and Interaction Patterns**
|
||||
|
||||
1. **Tutorial 9 — Forms and Validation**
|
||||
|
||||
* Build a `<user-form>` component with multiple fields and validation logic.
|
||||
* Introduce unit tests for validation rules and error display.
|
||||
* Show how to test async validations (e.g., simulated API check).
|
||||
|
||||
2. **Tutorial 10 — Modal Dialogs and Overlays**
|
||||
|
||||
* Implement a `<modal-dialog>` web component using Lit and CSS transitions.
|
||||
* Demonstrate accessibility (`aria-*`) and keyboard interactions.
|
||||
* TDD focus: test open/close state, keyboard handling, and backdrop clicks.
|
||||
|
||||
3. **Tutorial 11 — Keyboard Navigation and Accessibility**
|
||||
|
||||
* Add keyboard shortcuts and focus management to existing components.
|
||||
* Test tab order, focus restoration, and ARIA attributes using jsdom.
|
||||
|
||||
4. **Tutorial 12 — Dynamic Lists and Reordering**
|
||||
|
||||
* Build a draggable `<sortable-list>` component.
|
||||
* Test DOM updates, drag/drop events, and final order assertions.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ **B. State, Data, and Logic**
|
||||
|
||||
5. **Tutorial 13 — Derived and Computed State**
|
||||
|
||||
* Extend the store to include computed values (e.g., counts, filters).
|
||||
* Teach memoization and how to test reactive derivations.
|
||||
|
||||
6. **Tutorial 14 — Undo/Redo and Time Travel**
|
||||
|
||||
* Add a store history stack and commands for undo/redo.
|
||||
* Test state rollbacks deterministically.
|
||||
|
||||
7. **Tutorial 15 — Multi-Store Coordination**
|
||||
|
||||
* Split state across multiple stores (e.g., `UserStore`, `SettingsStore`).
|
||||
* Show how to compose subscriptions and test inter-store dependencies.
|
||||
|
||||
8. **Tutorial 16 — Data Fetching and Caching**
|
||||
|
||||
* Use `fetch()` to load data asynchronously and cache it in the store.
|
||||
* Mock HTTP requests in tests.
|
||||
* Discuss retry and error handling patterns.
|
||||
|
||||
---
|
||||
|
||||
## 💎 **C. Styling, Theming, and Branding**
|
||||
|
||||
9. **Tutorial 17 — Theming and CSS Variables**
|
||||
|
||||
* Implement a theme manager (light/dark/custom colors).
|
||||
* Test dynamic theme changes via store and DOM styles.
|
||||
|
||||
10. **Tutorial 18 — Component Libraries and Design Tokens**
|
||||
|
||||
* Introduce reusable style tokens and component variants.
|
||||
* Build visual regression tests using Storybook snapshots.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 **D. Architecture and Automation**
|
||||
|
||||
11. **Tutorial 19 — Reactive Controllers and Composition**
|
||||
|
||||
* Use Lit’s `ReactiveController` pattern to encapsulate logic like stores or timers.
|
||||
* TDD pattern: one controller, multiple components.
|
||||
|
||||
12. **Tutorial 20 — Custom Build and Test Pipelines**
|
||||
|
||||
* Show how to integrate Mocha tests into CI (GitHub Actions).
|
||||
* Include code coverage (nyc) and automatic agent test reporting.
|
||||
|
||||
13. **Tutorial 21 — Agentic Refactoring**
|
||||
|
||||
* Demonstrate agents proposing architectural changes:
|
||||
|
||||
* identifying code smells,
|
||||
* extracting controllers,
|
||||
* enforcing consistent store usage.
|
||||
|
||||
14. **Tutorial 22 — Agent-Driven Story Generation**
|
||||
|
||||
* Agents create new Storybook stories based on test cases.
|
||||
* Bridge test specs and UX documentation automatically.
|
||||
|
||||
---
|
||||
|
||||
## 🔮 **E. Integration and Expansion**
|
||||
|
||||
15. **Tutorial 23 — REST and GraphQL API Integration**
|
||||
|
||||
* Fetch and render real backend data.
|
||||
* Mock responses for offline testing.
|
||||
* Introduce contract tests for schema validation.
|
||||
|
||||
16. **Tutorial 24 — Internationalization (i18n)**
|
||||
|
||||
* Add locale switching to greetings.
|
||||
* Test translation loading and pluralization behavior.
|
||||
|
||||
17. **Tutorial 25 — Progressive Web App (PWA) Integration**
|
||||
|
||||
* Cache store data offline.
|
||||
* Add service worker tests for persistence.
|
||||
|
||||
18. **Tutorial 26 — Performance and Profiling**
|
||||
|
||||
* Measure component render times.
|
||||
* Write regression tests for performance thresholds.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **F. Visionary / Meta Layer**
|
||||
|
||||
19. **Tutorial 27 — Visual AI Testing**
|
||||
|
||||
* Integrate image snapshots from Storybook for visual regression comparison.
|
||||
* Show how agents can detect UI drift.
|
||||
|
||||
20. **Tutorial 28 — Self-Testing Components**
|
||||
|
||||
* Each component ships with its own self-test harness.
|
||||
* Components can verify their own integrity in isolation.
|
||||
|
||||
21. **Tutorial 29 — AI-Assisted UX Heuristics**
|
||||
|
||||
* Agents analyze user interaction logs and propose UI simplifications.
|
||||
* TDD for heuristic improvements.
|
||||
|
||||
22. **Tutorial 30 — Meta-Framework Evolution**
|
||||
|
||||
* Turn TestDrive-UI into a reusable CLI (`npx testdrive-ui new <component>`).
|
||||
* Generate scaffolds, tests, and stories automatically.
|
||||
|
||||
xxx
|
||||
List of **next-stage tutorial ideas**, organized by **theme** and **complexity**, to help evolve both the teaching path and the framework.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 **A. UI and Interaction Patterns**
|
||||
|
||||
1. **Tutorial 9 — Forms and Validation**
|
||||
|
||||
* Build a `<user-form>` component with multiple fields and validation logic.
|
||||
* Introduce unit tests for validation rules and error display.
|
||||
* Show how to test async validations (e.g., simulated API check).
|
||||
|
||||
2. **Tutorial 10 — Modal Dialogs and Overlays**
|
||||
|
||||
* Implement a `<modal-dialog>` web component using Lit and CSS transitions.
|
||||
* Demonstrate accessibility (`aria-*`) and keyboard interactions.
|
||||
* TDD focus: test open/close state, keyboard handling, and backdrop clicks.
|
||||
|
||||
3. **Tutorial 11 — Keyboard Navigation and Accessibility**
|
||||
|
||||
* Add keyboard shortcuts and focus management to existing components.
|
||||
* Test tab order, focus restoration, and ARIA attributes using jsdom.
|
||||
|
||||
4. **Tutorial 12 — Dynamic Lists and Reordering**
|
||||
|
||||
* Build a draggable `<sortable-list>` component.
|
||||
* Test DOM updates, drag/drop events, and final order assertions.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ **B. State, Data, and Logic**
|
||||
|
||||
5. **Tutorial 13 — Derived and Computed State**
|
||||
|
||||
* Extend the store to include computed values (e.g., counts, filters).
|
||||
* Teach memoization and how to test reactive derivations.
|
||||
|
||||
6. **Tutorial 14 — Undo/Redo and Time Travel**
|
||||
|
||||
* Add a store history stack and commands for undo/redo.
|
||||
* Test state rollbacks deterministically.
|
||||
|
||||
7. **Tutorial 15 — Multi-Store Coordination**
|
||||
|
||||
* Split state across multiple stores (e.g., `UserStore`, `SettingsStore`).
|
||||
* Show how to compose subscriptions and test inter-store dependencies.
|
||||
|
||||
8. **Tutorial 16 — Data Fetching and Caching**
|
||||
|
||||
* Use `fetch()` to load data asynchronously and cache it in the store.
|
||||
* Mock HTTP requests in tests.
|
||||
* Discuss retry and error handling patterns.
|
||||
|
||||
---
|
||||
|
||||
## 💎 **C. Styling, Theming, and Branding**
|
||||
|
||||
9. **Tutorial 17 — Theming and CSS Variables**
|
||||
|
||||
* Implement a theme manager (light/dark/custom colors).
|
||||
* Test dynamic theme changes via store and DOM styles.
|
||||
|
||||
10. **Tutorial 18 — Component Libraries and Design Tokens**
|
||||
|
||||
* Introduce reusable style tokens and component variants.
|
||||
* Build visual regression tests using Storybook snapshots.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 **D. Architecture and Automation**
|
||||
|
||||
11. **Tutorial 19 — Reactive Controllers and Composition**
|
||||
|
||||
* Use Lit’s `ReactiveController` pattern to encapsulate logic like stores or timers.
|
||||
* TDD pattern: one controller, multiple components.
|
||||
|
||||
12. **Tutorial 20 — Custom Build and Test Pipelines**
|
||||
|
||||
* Show how to integrate Mocha tests into CI (GitHub Actions).
|
||||
* Include code coverage (nyc) and automatic agent test reporting.
|
||||
|
||||
13. **Tutorial 21 — Agentic Refactoring**
|
||||
|
||||
* Demonstrate agents proposing architectural changes:
|
||||
|
||||
* identifying code smells,
|
||||
* extracting controllers,
|
||||
* enforcing consistent store usage.
|
||||
|
||||
14. **Tutorial 22 — Agent-Driven Story Generation**
|
||||
|
||||
* Agents create new Storybook stories based on test cases.
|
||||
* Bridge test specs and UX documentation automatically.
|
||||
|
||||
---
|
||||
|
||||
## 🔮 **E. Integration and Expansion**
|
||||
|
||||
15. **Tutorial 23 — REST and GraphQL API Integration**
|
||||
|
||||
* Fetch and render real backend data.
|
||||
* Mock responses for offline testing.
|
||||
* Introduce contract tests for schema validation.
|
||||
|
||||
16. **Tutorial 24 — Internationalization (i18n)**
|
||||
|
||||
* Add locale switching to greetings.
|
||||
* Test translation loading and pluralization behavior.
|
||||
|
||||
17. **Tutorial 25 — Progressive Web App (PWA) Integration**
|
||||
|
||||
* Cache store data offline.
|
||||
* Add service worker tests for persistence.
|
||||
|
||||
18. **Tutorial 26 — Performance and Profiling**
|
||||
|
||||
* Measure component render times.
|
||||
* Write regression tests for performance thresholds.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **F. Visionary / Meta Layer**
|
||||
|
||||
19. **Tutorial 27 — Visual AI Testing**
|
||||
|
||||
* Integrate image snapshots from Storybook for visual regression comparison.
|
||||
* Show how agents can detect UI drift.
|
||||
|
||||
20. **Tutorial 28 — Self-Testing Components**
|
||||
|
||||
* Each component ships with its own self-test harness.
|
||||
* Components can verify their own integrity in isolation.
|
||||
|
||||
21. **Tutorial 29 — AI-Assisted UX Heuristics**
|
||||
|
||||
* Agents analyze user interaction logs and propose UI simplifications.
|
||||
* TDD for heuristic improvements.
|
||||
|
||||
22. **Tutorial 30 — Meta-Framework Evolution**
|
||||
|
||||
* Turn TestDrive-UI into a reusable CLI (`npx testdrive-ui new <component>`).
|
||||
* Generate scaffolds, tests, and stories automatically.
|
||||
|
||||
xxx
|
||||
|
||||
@@ -1,187 +1,187 @@
|
||||
# Tutorial 1: Hello World!
|
||||
|
||||
Let’s walk through your first **HelloWorld** component built with **TestDrive-UI**.
|
||||
|
||||
This will demonstrate the full workflow:
|
||||
👉 requirement → test → implementation → run → refinement.
|
||||
|
||||
---
|
||||
|
||||
## 🧱 1. Project setup
|
||||
|
||||
If you haven’t already:
|
||||
|
||||
```bash
|
||||
unzip testdrive-ui.zip
|
||||
cd testdrive-ui
|
||||
npm install
|
||||
```
|
||||
|
||||
Now you’re ready to create your first component.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 2. Create a component folder
|
||||
|
||||
```bash
|
||||
mkdir src/components/hello-world
|
||||
```
|
||||
|
||||
Inside it, you’ll have:
|
||||
|
||||
```
|
||||
hello-world/
|
||||
├── hello-world.js
|
||||
├── hello-world.test.js
|
||||
└── hello-world.stories.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✏️ 3. Step 1 — Write the requirement
|
||||
|
||||
> “When `<hello-world>` is rendered, it should display the text **Hello World!**
|
||||
> and clicking the element should show an alert.”
|
||||
|
||||
That’s your behavioral spec — the *“why”* that drives the test.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 4. Step 2 — Write the test first
|
||||
|
||||
`src/components/hello-world/hello-world.test.js`
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
describe("<hello-world>", () => {
|
||||
it("renders the correct greeting", () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const content = el.shadowRoot.textContent;
|
||||
expect(content).to.include("Hello World!");
|
||||
});
|
||||
|
||||
it("triggers an alert on click", () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
|
||||
let alerted = false;
|
||||
window.alert = () => (alerted = true);
|
||||
|
||||
const div = el.shadowRoot.querySelector("div");
|
||||
div.click();
|
||||
|
||||
expect(alerted).to.be.true;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run this test now:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Both tests will **fail** initially — perfect, that’s the TDD start.
|
||||
|
||||
---
|
||||
|
||||
## 💡 5. Step 3 — Implement the component
|
||||
|
||||
`src/components/hello-world/hello-world.js`
|
||||
|
||||
```javascript
|
||||
import { LitElement, html, css } from "lit";
|
||||
|
||||
export class HelloWorld extends LitElement {
|
||||
static styles = css`
|
||||
div {
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
color: #007acc;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
div:hover {
|
||||
color: #005fa3;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`<div @click=${this._onClick}>Hello World!</div>`;
|
||||
}
|
||||
|
||||
_onClick() {
|
||||
alert("Hello from TestDrive-UI!");
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hello-world", HelloWorld);
|
||||
```
|
||||
|
||||
Run the tests again:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ Both should now pass.
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 6. Step 4 — Preview it in the browser
|
||||
|
||||
Add it to your `index.html`:
|
||||
|
||||
```html
|
||||
<script type="module" src="./components/hello-world/hello-world.js"></script>
|
||||
<hello-world></hello-world>
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:5173](http://localhost:5173)
|
||||
→ You’ll see your clickable **Hello World!** component rendered live.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 7. Step 5 — Visual story (optional)
|
||||
|
||||
`src/components/hello-world/hello-world.stories.js`
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
export default {
|
||||
title: "UI/Hello World",
|
||||
};
|
||||
|
||||
export const Default = () => `<hello-world></hello-world>`;
|
||||
```
|
||||
|
||||
Once you add Storybook to the scaffold later, this file will automatically generate a visual preview card.
|
||||
|
||||
---
|
||||
|
||||
## ✅ 8. What you achieved
|
||||
|
||||
* Created an **independent UI component** with Lit
|
||||
* Wrote deterministic **tests with jsdom + Mocha**
|
||||
* Verified behavior automatically
|
||||
* Previewed visually via **Vite**
|
||||
|
||||
This loop is your core **TestDrive-UI workflow**:
|
||||
|
||||
```
|
||||
spec → test → implement → run → refine
|
||||
```
|
||||
|
||||
xxx
|
||||
# Tutorial 1: Hello World!
|
||||
|
||||
Let’s walk through your first **HelloWorld** component built with **TestDrive-UI**.
|
||||
|
||||
This will demonstrate the full workflow:
|
||||
👉 requirement → test → implementation → run → refinement.
|
||||
|
||||
---
|
||||
|
||||
## 🧱 1. Project setup
|
||||
|
||||
If you haven’t already:
|
||||
|
||||
```bash
|
||||
unzip testdrive-ui.zip
|
||||
cd testdrive-ui
|
||||
npm install
|
||||
```
|
||||
|
||||
Now you’re ready to create your first component.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 2. Create a component folder
|
||||
|
||||
```bash
|
||||
mkdir src/components/hello-world
|
||||
```
|
||||
|
||||
Inside it, you’ll have:
|
||||
|
||||
```
|
||||
hello-world/
|
||||
├── hello-world.js
|
||||
├── hello-world.test.js
|
||||
└── hello-world.stories.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✏️ 3. Step 1 — Write the requirement
|
||||
|
||||
> “When `<hello-world>` is rendered, it should display the text **Hello World!**
|
||||
> and clicking the element should show an alert.”
|
||||
|
||||
That’s your behavioral spec — the *“why”* that drives the test.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 4. Step 2 — Write the test first
|
||||
|
||||
`src/components/hello-world/hello-world.test.js`
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
describe("<hello-world>", () => {
|
||||
it("renders the correct greeting", () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const content = el.shadowRoot.textContent;
|
||||
expect(content).to.include("Hello World!");
|
||||
});
|
||||
|
||||
it("triggers an alert on click", () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
|
||||
let alerted = false;
|
||||
window.alert = () => (alerted = true);
|
||||
|
||||
const div = el.shadowRoot.querySelector("div");
|
||||
div.click();
|
||||
|
||||
expect(alerted).to.be.true;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run this test now:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Both tests will **fail** initially — perfect, that’s the TDD start.
|
||||
|
||||
---
|
||||
|
||||
## 💡 5. Step 3 — Implement the component
|
||||
|
||||
`src/components/hello-world/hello-world.js`
|
||||
|
||||
```javascript
|
||||
import { LitElement, html, css } from "lit";
|
||||
|
||||
export class HelloWorld extends LitElement {
|
||||
static styles = css`
|
||||
div {
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
color: #007acc;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
div:hover {
|
||||
color: #005fa3;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`<div @click=${this._onClick}>Hello World!</div>`;
|
||||
}
|
||||
|
||||
_onClick() {
|
||||
alert("Hello from TestDrive-UI!");
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hello-world", HelloWorld);
|
||||
```
|
||||
|
||||
Run the tests again:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ Both should now pass.
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 6. Step 4 — Preview it in the browser
|
||||
|
||||
Add it to your `index.html`:
|
||||
|
||||
```html
|
||||
<script type="module" src="./components/hello-world/hello-world.js"></script>
|
||||
<hello-world></hello-world>
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:5173](http://localhost:5173)
|
||||
→ You’ll see your clickable **Hello World!** component rendered live.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 7. Step 5 — Visual story (optional)
|
||||
|
||||
`src/components/hello-world/hello-world.stories.js`
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
export default {
|
||||
title: "UI/Hello World",
|
||||
};
|
||||
|
||||
export const Default = () => `<hello-world></hello-world>`;
|
||||
```
|
||||
|
||||
Once you add Storybook to the scaffold later, this file will automatically generate a visual preview card.
|
||||
|
||||
---
|
||||
|
||||
## ✅ 8. What you achieved
|
||||
|
||||
* Created an **independent UI component** with Lit
|
||||
* Wrote deterministic **tests with jsdom + Mocha**
|
||||
* Verified behavior automatically
|
||||
* Previewed visually via **Vite**
|
||||
|
||||
This loop is your core **TestDrive-UI workflow**:
|
||||
|
||||
```
|
||||
spec → test → implement → run → refine
|
||||
```
|
||||
|
||||
xxx
|
||||
|
||||
@@ -1,217 +1,217 @@
|
||||
# Tutorial 2 Reactive Properties
|
||||
|
||||
This second **TestDrive-UI** tutorial extends your previous `hello-world` component by introducing **reactive properties** (i.e., component inputs) and **dynamic rendering**, all under **TDD** control.
|
||||
|
||||
We’ll end up with a `<hello-world name="Kale"></hello-world>` component that greets a given name — and can change dynamically when the property updates.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 1. Goal
|
||||
|
||||
> The component should display “Hello, [name]!”
|
||||
> and automatically update when the `name` property changes.
|
||||
|
||||
If no `name` is given, it should default to “World”.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 2. Step 1 — Write the failing test first
|
||||
|
||||
Create `src/components/hello-world/hello-world.props.test.js`:
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
describe("<hello-world> (with name property)", () => {
|
||||
it("renders default greeting when no name is set", () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const text = el.shadowRoot.textContent.trim();
|
||||
expect(text).to.equal("Hello, World!");
|
||||
});
|
||||
|
||||
it("renders custom greeting when name is set", async () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
el.name = "Kale";
|
||||
|
||||
// Wait for Lit’s update cycle
|
||||
await el.updateComplete;
|
||||
|
||||
const text = el.shadowRoot.textContent.trim();
|
||||
expect(text).to.equal("Hello, Kale!");
|
||||
});
|
||||
|
||||
it("reacts to property change after initial render", async () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
el.name = "Aria";
|
||||
await el.updateComplete;
|
||||
|
||||
let text = el.shadowRoot.textContent.trim();
|
||||
expect(text).to.equal("Hello, Aria!");
|
||||
|
||||
el.name = "Nova";
|
||||
await el.updateComplete;
|
||||
|
||||
text = el.shadowRoot.textContent.trim();
|
||||
expect(text).to.equal("Hello, Nova!");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests should fail — we haven’t implemented anything yet.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 3. Step 2 — Implement the feature
|
||||
|
||||
Open your existing `src/components/hello-world/hello-world.js`
|
||||
and replace the class with this improved version:
|
||||
|
||||
```javascript
|
||||
import { LitElement, html, css } from "lit";
|
||||
|
||||
export class HelloWorld extends LitElement {
|
||||
static properties = {
|
||||
name: { type: String }
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.name = "World";
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
div {
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
color: #007acc;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
div:hover {
|
||||
color: #005fa3;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`<div @click=${this._onClick}>
|
||||
Hello, ${this.name}!
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_onClick() {
|
||||
alert(`Hello, ${this.name}!`);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hello-world", HelloWorld);
|
||||
```
|
||||
|
||||
Run the tests again:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ All should now pass.
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 4. Step 3 — Try it live
|
||||
|
||||
Edit `src/index.html` to demonstrate both variants:
|
||||
|
||||
```html
|
||||
<hello-world></hello-world>
|
||||
<hello-world name="Coulomb"></hello-world>
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
In the browser you’ll see:
|
||||
|
||||
```
|
||||
Hello, World!
|
||||
Hello, Coulomb!
|
||||
```
|
||||
|
||||
and both are clickable.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 5. Step 4 — Live updates (optional exploration)
|
||||
|
||||
Open the browser console and type:
|
||||
|
||||
```javascript
|
||||
document.querySelector("hello-world").name = "Agent";
|
||||
```
|
||||
|
||||
The first greeting should **update instantly** to:
|
||||
|
||||
```
|
||||
Hello, Agent!
|
||||
```
|
||||
|
||||
That’s Lit’s reactive update mechanism at work.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 6. Step 5 — Visual story (optional)
|
||||
|
||||
`src/components/hello-world/hello-world.stories.js`
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
export default {
|
||||
title: "UI/Hello World (Reactive)"
|
||||
};
|
||||
|
||||
export const Default = () => `<hello-world></hello-world>`;
|
||||
export const CustomName = () => `<hello-world name="Bernd"></hello-world>`;
|
||||
```
|
||||
|
||||
If Storybook is later installed, these stories will become live demos.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 7. Key Takeaways
|
||||
|
||||
| Concept | Explanation |
|
||||
| --------------------- | ------------------------------------------------- |
|
||||
| **Reactive property** | Declared via `static properties = { ... }` in Lit |
|
||||
| **Default values** | Set in the constructor |
|
||||
| **Automatic updates** | Changing the property triggers re-render |
|
||||
| **Testing updates** | Use `await el.updateComplete` before asserting |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 8. What you learned
|
||||
|
||||
* How to **declare reactive component properties**
|
||||
* How to **test reactivity** with Mocha + jsdom
|
||||
* How to **update and verify UI behavior** in a TDD loop
|
||||
|
||||
---
|
||||
|
||||
Next, we can take it one level further:
|
||||
|
||||
> Add a **text input** inside `<hello-world>` that updates the `name` property live when the user types.
|
||||
|
||||
# Tutorial 2 Reactive Properties
|
||||
|
||||
This second **TestDrive-UI** tutorial extends your previous `hello-world` component by introducing **reactive properties** (i.e., component inputs) and **dynamic rendering**, all under **TDD** control.
|
||||
|
||||
We’ll end up with a `<hello-world name="Kale"></hello-world>` component that greets a given name — and can change dynamically when the property updates.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 1. Goal
|
||||
|
||||
> The component should display “Hello, [name]!”
|
||||
> and automatically update when the `name` property changes.
|
||||
|
||||
If no `name` is given, it should default to “World”.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 2. Step 1 — Write the failing test first
|
||||
|
||||
Create `src/components/hello-world/hello-world.props.test.js`:
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
describe("<hello-world> (with name property)", () => {
|
||||
it("renders default greeting when no name is set", () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const text = el.shadowRoot.textContent.trim();
|
||||
expect(text).to.equal("Hello, World!");
|
||||
});
|
||||
|
||||
it("renders custom greeting when name is set", async () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
el.name = "Kale";
|
||||
|
||||
// Wait for Lit’s update cycle
|
||||
await el.updateComplete;
|
||||
|
||||
const text = el.shadowRoot.textContent.trim();
|
||||
expect(text).to.equal("Hello, Kale!");
|
||||
});
|
||||
|
||||
it("reacts to property change after initial render", async () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
el.name = "Aria";
|
||||
await el.updateComplete;
|
||||
|
||||
let text = el.shadowRoot.textContent.trim();
|
||||
expect(text).to.equal("Hello, Aria!");
|
||||
|
||||
el.name = "Nova";
|
||||
await el.updateComplete;
|
||||
|
||||
text = el.shadowRoot.textContent.trim();
|
||||
expect(text).to.equal("Hello, Nova!");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests should fail — we haven’t implemented anything yet.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 3. Step 2 — Implement the feature
|
||||
|
||||
Open your existing `src/components/hello-world/hello-world.js`
|
||||
and replace the class with this improved version:
|
||||
|
||||
```javascript
|
||||
import { LitElement, html, css } from "lit";
|
||||
|
||||
export class HelloWorld extends LitElement {
|
||||
static properties = {
|
||||
name: { type: String }
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.name = "World";
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
div {
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
color: #007acc;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
div:hover {
|
||||
color: #005fa3;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`<div @click=${this._onClick}>
|
||||
Hello, ${this.name}!
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_onClick() {
|
||||
alert(`Hello, ${this.name}!`);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hello-world", HelloWorld);
|
||||
```
|
||||
|
||||
Run the tests again:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ All should now pass.
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 4. Step 3 — Try it live
|
||||
|
||||
Edit `src/index.html` to demonstrate both variants:
|
||||
|
||||
```html
|
||||
<hello-world></hello-world>
|
||||
<hello-world name="Coulomb"></hello-world>
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
In the browser you’ll see:
|
||||
|
||||
```
|
||||
Hello, World!
|
||||
Hello, Coulomb!
|
||||
```
|
||||
|
||||
and both are clickable.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 5. Step 4 — Live updates (optional exploration)
|
||||
|
||||
Open the browser console and type:
|
||||
|
||||
```javascript
|
||||
document.querySelector("hello-world").name = "Agent";
|
||||
```
|
||||
|
||||
The first greeting should **update instantly** to:
|
||||
|
||||
```
|
||||
Hello, Agent!
|
||||
```
|
||||
|
||||
That’s Lit’s reactive update mechanism at work.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 6. Step 5 — Visual story (optional)
|
||||
|
||||
`src/components/hello-world/hello-world.stories.js`
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
export default {
|
||||
title: "UI/Hello World (Reactive)"
|
||||
};
|
||||
|
||||
export const Default = () => `<hello-world></hello-world>`;
|
||||
export const CustomName = () => `<hello-world name="Bernd"></hello-world>`;
|
||||
```
|
||||
|
||||
If Storybook is later installed, these stories will become live demos.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 7. Key Takeaways
|
||||
|
||||
| Concept | Explanation |
|
||||
| --------------------- | ------------------------------------------------- |
|
||||
| **Reactive property** | Declared via `static properties = { ... }` in Lit |
|
||||
| **Default values** | Set in the constructor |
|
||||
| **Automatic updates** | Changing the property triggers re-render |
|
||||
| **Testing updates** | Use `await el.updateComplete` before asserting |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 8. What you learned
|
||||
|
||||
* How to **declare reactive component properties**
|
||||
* How to **test reactivity** with Mocha + jsdom
|
||||
* How to **update and verify UI behavior** in a TDD loop
|
||||
|
||||
---
|
||||
|
||||
Next, we can take it one level further:
|
||||
|
||||
> Add a **text input** inside `<hello-world>` that updates the `name` property live when the user types.
|
||||
|
||||
xxx
|
||||
@@ -1,221 +1,221 @@
|
||||
# Tutorial 3: Bi-Directional Data Binding
|
||||
|
||||
This **third tutorial** builds directly on your `hello-world` component and introduces **two-way interaction** (bi-directional data binding).
|
||||
|
||||
You’ll learn how to let the user type into an input field and see the greeting update live — while continuing to follow a **TDD-first** approach.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 1. Goal
|
||||
|
||||
> The `<hello-world>` component should display a greeting and an input box.
|
||||
> Typing into the input should update the greeting **in real time**.
|
||||
> The `name` property should still be readable/writable programmatically.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 2. Step 1 — Write the failing test first
|
||||
|
||||
Create a new test file:
|
||||
`src/components/hello-world/hello-world.input.test.js`
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
describe("<hello-world> (interactive input)", () => {
|
||||
it("renders an input element and shows default greeting", () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const input = el.shadowRoot.querySelector("input");
|
||||
const greeting = el.shadowRoot.querySelector(".greeting");
|
||||
|
||||
expect(input).to.exist;
|
||||
expect(greeting.textContent.trim()).to.equal("Hello, World!");
|
||||
});
|
||||
|
||||
it("updates greeting when user types into input", async () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const input = el.shadowRoot.querySelector("input");
|
||||
input.value = "Agent";
|
||||
input.dispatchEvent(new Event("input"));
|
||||
await el.updateComplete;
|
||||
|
||||
const greeting = el.shadowRoot.querySelector(".greeting");
|
||||
expect(greeting.textContent.trim()).to.equal("Hello, Agent!");
|
||||
});
|
||||
|
||||
it("reflects property changes in input value", async () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
|
||||
el.name = "Nova";
|
||||
await el.updateComplete;
|
||||
|
||||
const input = el.shadowRoot.querySelector("input");
|
||||
expect(input.value).to.equal("Nova");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
They will all **fail** — as expected.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 3. Step 2 — Implement the feature
|
||||
|
||||
Edit `src/components/hello-world/hello-world.js` and replace the render logic:
|
||||
|
||||
```javascript
|
||||
import { LitElement, html, css } from "lit";
|
||||
|
||||
export class HelloWorld extends LitElement {
|
||||
static properties = {
|
||||
name: { type: String }
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.name = "World";
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
color: #007acc;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
input {
|
||||
font-size: 1rem;
|
||||
margin-top: 1rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
width: 60%;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="greeting">Hello, ${this.name}!</div>
|
||||
<input
|
||||
type="text"
|
||||
.value=${this.name}
|
||||
@input=${this._onInput}
|
||||
aria-label="Name input"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_onInput(event) {
|
||||
this.name = event.target.value;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hello-world", HelloWorld);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 4. Step 3 — Run the tests again
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ All three tests should now pass.
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 5. Step 4 — Try it live
|
||||
|
||||
Update `src/index.html` to:
|
||||
|
||||
```html
|
||||
<hello-world></hello-world>
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
In your browser:
|
||||
|
||||
* You’ll see “Hello, World!”
|
||||
* Type “Coulomb” in the input box.
|
||||
* The greeting updates instantly: **Hello, Coulomb!**
|
||||
|
||||
---
|
||||
|
||||
## 🧠 6. Step 5 — Explore two-way binding manually
|
||||
|
||||
Open DevTools Console and run:
|
||||
|
||||
```javascript
|
||||
document.querySelector("hello-world").name = "Bernd";
|
||||
```
|
||||
|
||||
The input value updates automatically, and the greeting reflects the change too.
|
||||
That’s the power of Lit’s **reactive updates** combined with the native DOM event loop.
|
||||
|
||||
---
|
||||
|
||||
## 📚 7. Step 6 — Optional Storybook story
|
||||
|
||||
`src/components/hello-world/hello-world.interactive.stories.js`
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
export default {
|
||||
title: "UI/Hello World (Interactive)"
|
||||
};
|
||||
|
||||
export const Interactive = () => `<hello-world></hello-world>`;
|
||||
```
|
||||
|
||||
When you add Storybook later, this story will provide a live, interactive playground.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 8. Key TDD lessons learned
|
||||
|
||||
| Concept | Explanation |
|
||||
| -------------------- | ------------------------------------------------------------ |
|
||||
| **Event testing** | Simulate user input with `dispatchEvent(new Event('input'))` |
|
||||
| **Reactive updates** | Use `await el.updateComplete` to wait for re-render |
|
||||
| **Two-way binding** | Property changes update DOM; DOM events update property |
|
||||
| **Isolation** | jsdom tests confirm behavior without running a browser |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 9. Summary
|
||||
|
||||
You’ve now completed:
|
||||
|
||||
1. Static rendering (`Hello World!`)
|
||||
2. Reactive property rendering (`Hello, ${name}!`)
|
||||
3. Bi-directional interaction (live input → UI → property → UI)
|
||||
|
||||
You can now **test-drive** any UI component using the same methodology.
|
||||
|
||||
---
|
||||
|
||||
Continue to the **fourth tutorial** on **component composition** — i.e., building a small dashboard that uses multiple custom components together, still under test Control.
|
||||
|
||||
# Tutorial 3: Bi-Directional Data Binding
|
||||
|
||||
This **third tutorial** builds directly on your `hello-world` component and introduces **two-way interaction** (bi-directional data binding).
|
||||
|
||||
You’ll learn how to let the user type into an input field and see the greeting update live — while continuing to follow a **TDD-first** approach.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 1. Goal
|
||||
|
||||
> The `<hello-world>` component should display a greeting and an input box.
|
||||
> Typing into the input should update the greeting **in real time**.
|
||||
> The `name` property should still be readable/writable programmatically.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 2. Step 1 — Write the failing test first
|
||||
|
||||
Create a new test file:
|
||||
`src/components/hello-world/hello-world.input.test.js`
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
describe("<hello-world> (interactive input)", () => {
|
||||
it("renders an input element and shows default greeting", () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const input = el.shadowRoot.querySelector("input");
|
||||
const greeting = el.shadowRoot.querySelector(".greeting");
|
||||
|
||||
expect(input).to.exist;
|
||||
expect(greeting.textContent.trim()).to.equal("Hello, World!");
|
||||
});
|
||||
|
||||
it("updates greeting when user types into input", async () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const input = el.shadowRoot.querySelector("input");
|
||||
input.value = "Agent";
|
||||
input.dispatchEvent(new Event("input"));
|
||||
await el.updateComplete;
|
||||
|
||||
const greeting = el.shadowRoot.querySelector(".greeting");
|
||||
expect(greeting.textContent.trim()).to.equal("Hello, Agent!");
|
||||
});
|
||||
|
||||
it("reflects property changes in input value", async () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
|
||||
el.name = "Nova";
|
||||
await el.updateComplete;
|
||||
|
||||
const input = el.shadowRoot.querySelector("input");
|
||||
expect(input.value).to.equal("Nova");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
They will all **fail** — as expected.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 3. Step 2 — Implement the feature
|
||||
|
||||
Edit `src/components/hello-world/hello-world.js` and replace the render logic:
|
||||
|
||||
```javascript
|
||||
import { LitElement, html, css } from "lit";
|
||||
|
||||
export class HelloWorld extends LitElement {
|
||||
static properties = {
|
||||
name: { type: String }
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.name = "World";
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
color: #007acc;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
input {
|
||||
font-size: 1rem;
|
||||
margin-top: 1rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
width: 60%;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="greeting">Hello, ${this.name}!</div>
|
||||
<input
|
||||
type="text"
|
||||
.value=${this.name}
|
||||
@input=${this._onInput}
|
||||
aria-label="Name input"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_onInput(event) {
|
||||
this.name = event.target.value;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hello-world", HelloWorld);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 4. Step 3 — Run the tests again
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ All three tests should now pass.
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 5. Step 4 — Try it live
|
||||
|
||||
Update `src/index.html` to:
|
||||
|
||||
```html
|
||||
<hello-world></hello-world>
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
In your browser:
|
||||
|
||||
* You’ll see “Hello, World!”
|
||||
* Type “Coulomb” in the input box.
|
||||
* The greeting updates instantly: **Hello, Coulomb!**
|
||||
|
||||
---
|
||||
|
||||
## 🧠 6. Step 5 — Explore two-way binding manually
|
||||
|
||||
Open DevTools Console and run:
|
||||
|
||||
```javascript
|
||||
document.querySelector("hello-world").name = "Bernd";
|
||||
```
|
||||
|
||||
The input value updates automatically, and the greeting reflects the change too.
|
||||
That’s the power of Lit’s **reactive updates** combined with the native DOM event loop.
|
||||
|
||||
---
|
||||
|
||||
## 📚 7. Step 6 — Optional Storybook story
|
||||
|
||||
`src/components/hello-world/hello-world.interactive.stories.js`
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
export default {
|
||||
title: "UI/Hello World (Interactive)"
|
||||
};
|
||||
|
||||
export const Interactive = () => `<hello-world></hello-world>`;
|
||||
```
|
||||
|
||||
When you add Storybook later, this story will provide a live, interactive playground.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 8. Key TDD lessons learned
|
||||
|
||||
| Concept | Explanation |
|
||||
| -------------------- | ------------------------------------------------------------ |
|
||||
| **Event testing** | Simulate user input with `dispatchEvent(new Event('input'))` |
|
||||
| **Reactive updates** | Use `await el.updateComplete` to wait for re-render |
|
||||
| **Two-way binding** | Property changes update DOM; DOM events update property |
|
||||
| **Isolation** | jsdom tests confirm behavior without running a browser |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 9. Summary
|
||||
|
||||
You’ve now completed:
|
||||
|
||||
1. Static rendering (`Hello World!`)
|
||||
2. Reactive property rendering (`Hello, ${name}!`)
|
||||
3. Bi-directional interaction (live input → UI → property → UI)
|
||||
|
||||
You can now **test-drive** any UI component using the same methodology.
|
||||
|
||||
---
|
||||
|
||||
Continue to the **fourth tutorial** on **component composition** — i.e., building a small dashboard that uses multiple custom components together, still under test Control.
|
||||
|
||||
xxx
|
||||
@@ -1,257 +1,257 @@
|
||||
# Tutorial 4: Component Composition
|
||||
|
||||
Welcome to the **fourth tutorial** for TestDriveUi building on the first three and introduces **component composition**.
|
||||
|
||||
You’ll learn how to make multiple Lit components **interact** while keeping the development **test-driven** and agent-friendly.
|
||||
|
||||
---
|
||||
|
||||
# 🧩 Tutorial 4 — Composing Components
|
||||
|
||||
### “Hello Dashboard” — combining reusable components
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goal
|
||||
|
||||
We’ll create a small **dashboard** component called `<hello-dashboard>` that:
|
||||
|
||||
1. Renders multiple `<hello-world>` components.
|
||||
2. Tracks how many greetings are currently visible.
|
||||
3. Updates a counter when a new greeter is added.
|
||||
4. Uses **TDD** for structure and behavior.
|
||||
|
||||
In short:
|
||||
|
||||
> “A dashboard that shows several personalized greetings and a live counter.”
|
||||
|
||||
---
|
||||
|
||||
## 🧪 1. Step 1 — Write the failing test first
|
||||
|
||||
Create `src/components/hello-dashboard/hello-dashboard.test.js`:
|
||||
|
||||
```javascript
|
||||
import "../hello-world/hello-world.js";
|
||||
import "./hello-dashboard.js";
|
||||
|
||||
describe("<hello-dashboard>", () => {
|
||||
it("renders a header and a counter", () => {
|
||||
const el = document.createElement("hello-dashboard");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const header = el.shadowRoot.querySelector("h2");
|
||||
const counter = el.shadowRoot.querySelector(".counter");
|
||||
|
||||
expect(header.textContent).to.include("Hello Dashboard");
|
||||
expect(counter.textContent).to.match(/Total greetings:/);
|
||||
});
|
||||
|
||||
it("renders at least one <hello-world> component", () => {
|
||||
const el = document.createElement("hello-dashboard");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const greeters = el.shadowRoot.querySelectorAll("hello-world");
|
||||
expect(greeters.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it("adds a new greeter when the Add button is clicked", async () => {
|
||||
const el = document.createElement("hello-dashboard");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const addButton = el.shadowRoot.querySelector("button");
|
||||
addButton.click();
|
||||
await el.updateComplete;
|
||||
|
||||
const greeters = el.shadowRoot.querySelectorAll("hello-world");
|
||||
expect(greeters.length).to.equal(2);
|
||||
|
||||
const counter = el.shadowRoot.querySelector(".counter").textContent;
|
||||
expect(counter).to.include("2");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run it:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests fail (expected).
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 2. Step 2 — Implement `<hello-dashboard>`
|
||||
|
||||
Create a new folder:
|
||||
|
||||
```
|
||||
src/components/hello-dashboard/
|
||||
```
|
||||
|
||||
and add this file:
|
||||
`src/components/hello-dashboard/hello-dashboard.js`
|
||||
|
||||
```javascript
|
||||
import { LitElement, html, css } from "lit";
|
||||
import "../hello-world/hello-world.js";
|
||||
|
||||
export class HelloDashboard extends LitElement {
|
||||
static styles = css`
|
||||
.container {
|
||||
font-family: system-ui, sans-serif;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.counter {
|
||||
margin: 0.5rem 0 1rem 0;
|
||||
font-weight: bold;
|
||||
color: #007acc;
|
||||
}
|
||||
button {
|
||||
background: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 1rem;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
button:hover {
|
||||
background: #005fa3;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.greeters = ["World"];
|
||||
}
|
||||
|
||||
_addGreeter() {
|
||||
const newName = `User${this.greeters.length + 1}`;
|
||||
this.greeters = [...this.greeters, newName];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<h2>Hello Dashboard</h2>
|
||||
<div class="counter">Total greetings: ${this.greeters.length}</div>
|
||||
${this.greeters.map(
|
||||
(name) => html`<hello-world name=${name}></hello-world>`
|
||||
)}
|
||||
<button @click=${this._addGreeter}>Add Greeting</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hello-dashboard", HelloDashboard);
|
||||
```
|
||||
|
||||
Run tests again:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ All should now pass.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 3. Step 3 — Try it live
|
||||
|
||||
Add this to `src/index.html`:
|
||||
|
||||
```html
|
||||
<script type="module" src="./components/hello-dashboard/hello-dashboard.js"></script>
|
||||
<hello-dashboard></hello-dashboard>
|
||||
```
|
||||
|
||||
Then start the dev server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:5173](http://localhost:5173)
|
||||
You’ll see:
|
||||
|
||||
```
|
||||
Hello Dashboard
|
||||
Total greetings: 1
|
||||
Hello, World!
|
||||
[Add Greeting]
|
||||
```
|
||||
|
||||
Click **Add Greeting** — new greeters appear, and the counter updates automatically.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 4. Step 4 — Extend your test coverage (optional)
|
||||
|
||||
Add to `hello-dashboard.test.js`:
|
||||
|
||||
```javascript
|
||||
it("propagates custom names correctly", () => {
|
||||
const el = document.createElement("hello-dashboard");
|
||||
el.greeters = ["Alpha", "Beta", "Gamma"];
|
||||
document.body.appendChild(el);
|
||||
|
||||
const greeters = el.shadowRoot.querySelectorAll("hello-world");
|
||||
const names = Array.from(greeters).map((g) => g.getAttribute("name"));
|
||||
expect(names).to.deep.equal(["Alpha", "Beta", "Gamma"]);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 5. Step 5 — Optional Storybook story
|
||||
|
||||
`src/components/hello-dashboard/hello-dashboard.stories.js`:
|
||||
|
||||
```javascript
|
||||
import "./hello-dashboard.js";
|
||||
|
||||
export default {
|
||||
title: "UI/Hello Dashboard"
|
||||
};
|
||||
|
||||
export const Default = () => `<hello-dashboard></hello-dashboard>`;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧩 6. Lessons learned
|
||||
|
||||
| Concept | What you practiced |
|
||||
| --------------------- | ------------------------------------------- |
|
||||
| **Composition** | One Lit component rendering others |
|
||||
| **Reactive arrays** | Using property updates to trigger re-render |
|
||||
| **Dynamic templates** | Rendering lists with `.map()` |
|
||||
| **Event testing** | Simulating clicks and rechecking the DOM |
|
||||
| **Incremental TDD** | Adding small features one test at a time |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 7. Summary
|
||||
|
||||
You’ve now implemented:
|
||||
|
||||
1. A simple reactive component (`hello-world`)
|
||||
2. User interaction (input updates)
|
||||
3. Property-based rendering
|
||||
4. Component composition (`hello-dashboard`)
|
||||
|
||||
Your **TestDrive-UI** foundation now supports:
|
||||
|
||||
* Isolated unit tests (`Mocha + jsdom`)
|
||||
* Composed component testing
|
||||
* Interactive browser previews (`Vite`)
|
||||
|
||||
---
|
||||
|
||||
Continue to **Event communication** between components — e.g., the dashboard listening to events fired by each greeter when they change their name.
|
||||
|
||||
xxx
|
||||
# Tutorial 4: Component Composition
|
||||
|
||||
Welcome to the **fourth tutorial** for TestDriveUi building on the first three and introduces **component composition**.
|
||||
|
||||
You’ll learn how to make multiple Lit components **interact** while keeping the development **test-driven** and agent-friendly.
|
||||
|
||||
---
|
||||
|
||||
# 🧩 Tutorial 4 — Composing Components
|
||||
|
||||
### “Hello Dashboard” — combining reusable components
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goal
|
||||
|
||||
We’ll create a small **dashboard** component called `<hello-dashboard>` that:
|
||||
|
||||
1. Renders multiple `<hello-world>` components.
|
||||
2. Tracks how many greetings are currently visible.
|
||||
3. Updates a counter when a new greeter is added.
|
||||
4. Uses **TDD** for structure and behavior.
|
||||
|
||||
In short:
|
||||
|
||||
> “A dashboard that shows several personalized greetings and a live counter.”
|
||||
|
||||
---
|
||||
|
||||
## 🧪 1. Step 1 — Write the failing test first
|
||||
|
||||
Create `src/components/hello-dashboard/hello-dashboard.test.js`:
|
||||
|
||||
```javascript
|
||||
import "../hello-world/hello-world.js";
|
||||
import "./hello-dashboard.js";
|
||||
|
||||
describe("<hello-dashboard>", () => {
|
||||
it("renders a header and a counter", () => {
|
||||
const el = document.createElement("hello-dashboard");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const header = el.shadowRoot.querySelector("h2");
|
||||
const counter = el.shadowRoot.querySelector(".counter");
|
||||
|
||||
expect(header.textContent).to.include("Hello Dashboard");
|
||||
expect(counter.textContent).to.match(/Total greetings:/);
|
||||
});
|
||||
|
||||
it("renders at least one <hello-world> component", () => {
|
||||
const el = document.createElement("hello-dashboard");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const greeters = el.shadowRoot.querySelectorAll("hello-world");
|
||||
expect(greeters.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it("adds a new greeter when the Add button is clicked", async () => {
|
||||
const el = document.createElement("hello-dashboard");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const addButton = el.shadowRoot.querySelector("button");
|
||||
addButton.click();
|
||||
await el.updateComplete;
|
||||
|
||||
const greeters = el.shadowRoot.querySelectorAll("hello-world");
|
||||
expect(greeters.length).to.equal(2);
|
||||
|
||||
const counter = el.shadowRoot.querySelector(".counter").textContent;
|
||||
expect(counter).to.include("2");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run it:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests fail (expected).
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 2. Step 2 — Implement `<hello-dashboard>`
|
||||
|
||||
Create a new folder:
|
||||
|
||||
```
|
||||
src/components/hello-dashboard/
|
||||
```
|
||||
|
||||
and add this file:
|
||||
`src/components/hello-dashboard/hello-dashboard.js`
|
||||
|
||||
```javascript
|
||||
import { LitElement, html, css } from "lit";
|
||||
import "../hello-world/hello-world.js";
|
||||
|
||||
export class HelloDashboard extends LitElement {
|
||||
static styles = css`
|
||||
.container {
|
||||
font-family: system-ui, sans-serif;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.counter {
|
||||
margin: 0.5rem 0 1rem 0;
|
||||
font-weight: bold;
|
||||
color: #007acc;
|
||||
}
|
||||
button {
|
||||
background: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 1rem;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
button:hover {
|
||||
background: #005fa3;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.greeters = ["World"];
|
||||
}
|
||||
|
||||
_addGreeter() {
|
||||
const newName = `User${this.greeters.length + 1}`;
|
||||
this.greeters = [...this.greeters, newName];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<h2>Hello Dashboard</h2>
|
||||
<div class="counter">Total greetings: ${this.greeters.length}</div>
|
||||
${this.greeters.map(
|
||||
(name) => html`<hello-world name=${name}></hello-world>`
|
||||
)}
|
||||
<button @click=${this._addGreeter}>Add Greeting</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hello-dashboard", HelloDashboard);
|
||||
```
|
||||
|
||||
Run tests again:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ All should now pass.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 3. Step 3 — Try it live
|
||||
|
||||
Add this to `src/index.html`:
|
||||
|
||||
```html
|
||||
<script type="module" src="./components/hello-dashboard/hello-dashboard.js"></script>
|
||||
<hello-dashboard></hello-dashboard>
|
||||
```
|
||||
|
||||
Then start the dev server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:5173](http://localhost:5173)
|
||||
You’ll see:
|
||||
|
||||
```
|
||||
Hello Dashboard
|
||||
Total greetings: 1
|
||||
Hello, World!
|
||||
[Add Greeting]
|
||||
```
|
||||
|
||||
Click **Add Greeting** — new greeters appear, and the counter updates automatically.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 4. Step 4 — Extend your test coverage (optional)
|
||||
|
||||
Add to `hello-dashboard.test.js`:
|
||||
|
||||
```javascript
|
||||
it("propagates custom names correctly", () => {
|
||||
const el = document.createElement("hello-dashboard");
|
||||
el.greeters = ["Alpha", "Beta", "Gamma"];
|
||||
document.body.appendChild(el);
|
||||
|
||||
const greeters = el.shadowRoot.querySelectorAll("hello-world");
|
||||
const names = Array.from(greeters).map((g) => g.getAttribute("name"));
|
||||
expect(names).to.deep.equal(["Alpha", "Beta", "Gamma"]);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 5. Step 5 — Optional Storybook story
|
||||
|
||||
`src/components/hello-dashboard/hello-dashboard.stories.js`:
|
||||
|
||||
```javascript
|
||||
import "./hello-dashboard.js";
|
||||
|
||||
export default {
|
||||
title: "UI/Hello Dashboard"
|
||||
};
|
||||
|
||||
export const Default = () => `<hello-dashboard></hello-dashboard>`;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧩 6. Lessons learned
|
||||
|
||||
| Concept | What you practiced |
|
||||
| --------------------- | ------------------------------------------- |
|
||||
| **Composition** | One Lit component rendering others |
|
||||
| **Reactive arrays** | Using property updates to trigger re-render |
|
||||
| **Dynamic templates** | Rendering lists with `.map()` |
|
||||
| **Event testing** | Simulating clicks and rechecking the DOM |
|
||||
| **Incremental TDD** | Adding small features one test at a time |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 7. Summary
|
||||
|
||||
You’ve now implemented:
|
||||
|
||||
1. A simple reactive component (`hello-world`)
|
||||
2. User interaction (input updates)
|
||||
3. Property-based rendering
|
||||
4. Component composition (`hello-dashboard`)
|
||||
|
||||
Your **TestDrive-UI** foundation now supports:
|
||||
|
||||
* Isolated unit tests (`Mocha + jsdom`)
|
||||
* Composed component testing
|
||||
* Interactive browser previews (`Vite`)
|
||||
|
||||
---
|
||||
|
||||
Continue to **Event communication** between components — e.g., the dashboard listening to events fired by each greeter when they change their name.
|
||||
|
||||
xxx
|
||||
|
||||
@@ -1,252 +1,252 @@
|
||||
# Tutorial 5: Component Communication
|
||||
|
||||
Welcome to the **fifth tutorial** takes you into one of the most important patterns in UI development: **communication between components.**
|
||||
Up to now, your `<hello-world>` components have been independent. In this tutorial, you’ll make them *talk* to their parent `<hello-dashboard>` component via **custom events**, keeping everything under **TDD**.
|
||||
|
||||
---
|
||||
|
||||
# 🧭 Tutorial 5 — Component Communication
|
||||
|
||||
### “Talking Components” — events between parent and child
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goal
|
||||
|
||||
We’ll make each `<hello-world>` component **emit an event** when its name changes.
|
||||
The `<hello-dashboard>` component will **listen to these events** and maintain a live **activity log**.
|
||||
|
||||
The flow will look like this:
|
||||
|
||||
```
|
||||
[User types in <hello-world> input]
|
||||
↓
|
||||
<hello-world> fires event "name-changed"
|
||||
↓
|
||||
<hello-dashboard> listens, updates log
|
||||
↓
|
||||
Log list displays latest updates
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 1. Step 1 — Write the failing test first
|
||||
|
||||
Create `src/components/hello-dashboard/hello-dashboard.events.test.js`:
|
||||
|
||||
```javascript
|
||||
import "../hello-world/hello-world.js";
|
||||
import "./hello-dashboard.js";
|
||||
|
||||
describe("<hello-dashboard> (events)", () => {
|
||||
it("shows a log section that updates when a child emits 'name-changed'", async () => {
|
||||
const el = document.createElement("hello-dashboard");
|
||||
document.body.appendChild(el);
|
||||
|
||||
// Initial render
|
||||
let log = el.shadowRoot.querySelector(".log");
|
||||
expect(log.textContent).to.include("No activity yet");
|
||||
|
||||
// Simulate child event
|
||||
const firstChild = el.shadowRoot.querySelector("hello-world");
|
||||
const event = new CustomEvent("name-changed", {
|
||||
detail: { oldName: "World", newName: "Agent" },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
});
|
||||
firstChild.dispatchEvent(event);
|
||||
await el.updateComplete;
|
||||
|
||||
log = el.shadowRoot.querySelector(".log");
|
||||
expect(log.textContent).to.include("World → Agent");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
The test fails (expected).
|
||||
|
||||
---
|
||||
|
||||
## 🧩 2. Step 2 — Update `<hello-world>` to fire events
|
||||
|
||||
Open `src/components/hello-world/hello-world.js`
|
||||
and update `_onInput()` to emit an event whenever the name changes:
|
||||
|
||||
```javascript
|
||||
_onInput(event) {
|
||||
const oldName = this.name;
|
||||
this.name = event.target.value;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("name-changed", {
|
||||
detail: { oldName, newName: this.name },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
})
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
That’s it — now `hello-world` tells the world when its name changes.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 3. Step 3 — Update `<hello-dashboard>` to handle events
|
||||
|
||||
Edit `src/components/hello-dashboard/hello-dashboard.js`:
|
||||
|
||||
Add a new property and event handler:
|
||||
|
||||
```javascript
|
||||
constructor() {
|
||||
super();
|
||||
this.greeters = ["World"];
|
||||
this.logs = [];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("name-changed", this._onNameChanged);
|
||||
}
|
||||
|
||||
_onNameChanged = (e) => {
|
||||
const { oldName, newName } = e.detail;
|
||||
this.logs = [`${oldName} → ${newName}`, ...this.logs];
|
||||
};
|
||||
```
|
||||
|
||||
Now render the log area under the button:
|
||||
|
||||
```javascript
|
||||
render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<h2>Hello Dashboard</h2>
|
||||
<div class="counter">Total greetings: ${this.greeters.length}</div>
|
||||
${this.greeters.map(
|
||||
(name) => html`<hello-world name=${name}></hello-world>`
|
||||
)}
|
||||
<button @click=${this._addGreeter}>Add Greeting</button>
|
||||
|
||||
<div class="log">
|
||||
${this.logs.length === 0
|
||||
? html`<p>No activity yet</p>`
|
||||
: html`<ul>
|
||||
${this.logs.map((l) => html`<li>${l}</li>`)}
|
||||
</ul>`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
Run tests again:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ The event test should now pass.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 4. Step 4 — Try it live
|
||||
|
||||
In `src/index.html`:
|
||||
|
||||
```html
|
||||
<hello-dashboard></hello-dashboard>
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open the page —
|
||||
Type into a greeter input, and you’ll see a live **activity log** appear below the button:
|
||||
|
||||
```
|
||||
Hello Dashboard
|
||||
Total greetings: 1
|
||||
Hello, World!
|
||||
[ Add Greeting ]
|
||||
Activity Log:
|
||||
World → Agent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧠 5. Step 5 — Add a test for multiple events (optional)
|
||||
|
||||
Extend `hello-dashboard.events.test.js`:
|
||||
|
||||
```javascript
|
||||
it("keeps a running list of multiple name changes", async () => {
|
||||
const el = document.createElement("hello-dashboard");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const child = el.shadowRoot.querySelector("hello-world");
|
||||
|
||||
const events = [
|
||||
new CustomEvent("name-changed", { detail: { oldName: "World", newName: "A" }, bubbles: true, composed: true }),
|
||||
new CustomEvent("name-changed", { detail: { oldName: "A", newName: "B" }, bubbles: true, composed: true })
|
||||
];
|
||||
|
||||
child.dispatchEvent(events[0]);
|
||||
child.dispatchEvent(events[1]);
|
||||
await el.updateComplete;
|
||||
|
||||
const items = Array.from(el.shadowRoot.querySelectorAll(".log li")).map(li => li.textContent);
|
||||
expect(items).to.deep.equal(["A → B", "World → A"]);
|
||||
});
|
||||
```
|
||||
|
||||
✅ It should pass too.
|
||||
|
||||
---
|
||||
|
||||
## 🔩 6. Key Concepts
|
||||
|
||||
| Concept | Explanation |
|
||||
| -------------------------- | ----------------------------------------------------------------- |
|
||||
| **CustomEvent** | A way for child components to send structured messages to parents |
|
||||
| **Bubbling + composed** | Ensures events cross shadow DOM boundaries |
|
||||
| **Reactive logs** | Updating an array property triggers rerender |
|
||||
| **Hierarchical testing** | TDD across parent/child relationships |
|
||||
| **Single source of truth** | Dashboard owns state; children only emit events |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 7. Summary
|
||||
|
||||
You’ve now built:
|
||||
|
||||
1. Independent reactive components (`hello-world`)
|
||||
2. Composed layouts (`hello-dashboard`)
|
||||
3. Two-way UI interaction (input updates)
|
||||
4. **Inter-component event communication**
|
||||
|
||||
Your **TestDrive-UI** framework now supports:
|
||||
|
||||
* Pure front-end logic
|
||||
* Reactive re-rendering
|
||||
* Nested component tests
|
||||
* Event-driven state management
|
||||
|
||||
---
|
||||
|
||||
Next step (Tutorial 6) will cover:
|
||||
|
||||
> **State synchronization** — introducing a shared store or context (e.g. using Lit’s reactive controllers or a lightweight signal system) so that multiple components reflect shared data automatically.
|
||||
|
||||
Would you like to continue with that direction next?
|
||||
|
||||
# Tutorial 5: Component Communication
|
||||
|
||||
Welcome to the **fifth tutorial** takes you into one of the most important patterns in UI development: **communication between components.**
|
||||
Up to now, your `<hello-world>` components have been independent. In this tutorial, you’ll make them *talk* to their parent `<hello-dashboard>` component via **custom events**, keeping everything under **TDD**.
|
||||
|
||||
---
|
||||
|
||||
# 🧭 Tutorial 5 — Component Communication
|
||||
|
||||
### “Talking Components” — events between parent and child
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goal
|
||||
|
||||
We’ll make each `<hello-world>` component **emit an event** when its name changes.
|
||||
The `<hello-dashboard>` component will **listen to these events** and maintain a live **activity log**.
|
||||
|
||||
The flow will look like this:
|
||||
|
||||
```
|
||||
[User types in <hello-world> input]
|
||||
↓
|
||||
<hello-world> fires event "name-changed"
|
||||
↓
|
||||
<hello-dashboard> listens, updates log
|
||||
↓
|
||||
Log list displays latest updates
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 1. Step 1 — Write the failing test first
|
||||
|
||||
Create `src/components/hello-dashboard/hello-dashboard.events.test.js`:
|
||||
|
||||
```javascript
|
||||
import "../hello-world/hello-world.js";
|
||||
import "./hello-dashboard.js";
|
||||
|
||||
describe("<hello-dashboard> (events)", () => {
|
||||
it("shows a log section that updates when a child emits 'name-changed'", async () => {
|
||||
const el = document.createElement("hello-dashboard");
|
||||
document.body.appendChild(el);
|
||||
|
||||
// Initial render
|
||||
let log = el.shadowRoot.querySelector(".log");
|
||||
expect(log.textContent).to.include("No activity yet");
|
||||
|
||||
// Simulate child event
|
||||
const firstChild = el.shadowRoot.querySelector("hello-world");
|
||||
const event = new CustomEvent("name-changed", {
|
||||
detail: { oldName: "World", newName: "Agent" },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
});
|
||||
firstChild.dispatchEvent(event);
|
||||
await el.updateComplete;
|
||||
|
||||
log = el.shadowRoot.querySelector(".log");
|
||||
expect(log.textContent).to.include("World → Agent");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
The test fails (expected).
|
||||
|
||||
---
|
||||
|
||||
## 🧩 2. Step 2 — Update `<hello-world>` to fire events
|
||||
|
||||
Open `src/components/hello-world/hello-world.js`
|
||||
and update `_onInput()` to emit an event whenever the name changes:
|
||||
|
||||
```javascript
|
||||
_onInput(event) {
|
||||
const oldName = this.name;
|
||||
this.name = event.target.value;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("name-changed", {
|
||||
detail: { oldName, newName: this.name },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
})
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
That’s it — now `hello-world` tells the world when its name changes.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 3. Step 3 — Update `<hello-dashboard>` to handle events
|
||||
|
||||
Edit `src/components/hello-dashboard/hello-dashboard.js`:
|
||||
|
||||
Add a new property and event handler:
|
||||
|
||||
```javascript
|
||||
constructor() {
|
||||
super();
|
||||
this.greeters = ["World"];
|
||||
this.logs = [];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("name-changed", this._onNameChanged);
|
||||
}
|
||||
|
||||
_onNameChanged = (e) => {
|
||||
const { oldName, newName } = e.detail;
|
||||
this.logs = [`${oldName} → ${newName}`, ...this.logs];
|
||||
};
|
||||
```
|
||||
|
||||
Now render the log area under the button:
|
||||
|
||||
```javascript
|
||||
render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<h2>Hello Dashboard</h2>
|
||||
<div class="counter">Total greetings: ${this.greeters.length}</div>
|
||||
${this.greeters.map(
|
||||
(name) => html`<hello-world name=${name}></hello-world>`
|
||||
)}
|
||||
<button @click=${this._addGreeter}>Add Greeting</button>
|
||||
|
||||
<div class="log">
|
||||
${this.logs.length === 0
|
||||
? html`<p>No activity yet</p>`
|
||||
: html`<ul>
|
||||
${this.logs.map((l) => html`<li>${l}</li>`)}
|
||||
</ul>`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
Run tests again:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ The event test should now pass.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 4. Step 4 — Try it live
|
||||
|
||||
In `src/index.html`:
|
||||
|
||||
```html
|
||||
<hello-dashboard></hello-dashboard>
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open the page —
|
||||
Type into a greeter input, and you’ll see a live **activity log** appear below the button:
|
||||
|
||||
```
|
||||
Hello Dashboard
|
||||
Total greetings: 1
|
||||
Hello, World!
|
||||
[ Add Greeting ]
|
||||
Activity Log:
|
||||
World → Agent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧠 5. Step 5 — Add a test for multiple events (optional)
|
||||
|
||||
Extend `hello-dashboard.events.test.js`:
|
||||
|
||||
```javascript
|
||||
it("keeps a running list of multiple name changes", async () => {
|
||||
const el = document.createElement("hello-dashboard");
|
||||
document.body.appendChild(el);
|
||||
|
||||
const child = el.shadowRoot.querySelector("hello-world");
|
||||
|
||||
const events = [
|
||||
new CustomEvent("name-changed", { detail: { oldName: "World", newName: "A" }, bubbles: true, composed: true }),
|
||||
new CustomEvent("name-changed", { detail: { oldName: "A", newName: "B" }, bubbles: true, composed: true })
|
||||
];
|
||||
|
||||
child.dispatchEvent(events[0]);
|
||||
child.dispatchEvent(events[1]);
|
||||
await el.updateComplete;
|
||||
|
||||
const items = Array.from(el.shadowRoot.querySelectorAll(".log li")).map(li => li.textContent);
|
||||
expect(items).to.deep.equal(["A → B", "World → A"]);
|
||||
});
|
||||
```
|
||||
|
||||
✅ It should pass too.
|
||||
|
||||
---
|
||||
|
||||
## 🔩 6. Key Concepts
|
||||
|
||||
| Concept | Explanation |
|
||||
| -------------------------- | ----------------------------------------------------------------- |
|
||||
| **CustomEvent** | A way for child components to send structured messages to parents |
|
||||
| **Bubbling + composed** | Ensures events cross shadow DOM boundaries |
|
||||
| **Reactive logs** | Updating an array property triggers rerender |
|
||||
| **Hierarchical testing** | TDD across parent/child relationships |
|
||||
| **Single source of truth** | Dashboard owns state; children only emit events |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 7. Summary
|
||||
|
||||
You’ve now built:
|
||||
|
||||
1. Independent reactive components (`hello-world`)
|
||||
2. Composed layouts (`hello-dashboard`)
|
||||
3. Two-way UI interaction (input updates)
|
||||
4. **Inter-component event communication**
|
||||
|
||||
Your **TestDrive-UI** framework now supports:
|
||||
|
||||
* Pure front-end logic
|
||||
* Reactive re-rendering
|
||||
* Nested component tests
|
||||
* Event-driven state management
|
||||
|
||||
---
|
||||
|
||||
Next step (Tutorial 6) will cover:
|
||||
|
||||
> **State synchronization** — introducing a shared store or context (e.g. using Lit’s reactive controllers or a lightweight signal system) so that multiple components reflect shared data automatically.
|
||||
|
||||
Would you like to continue with that direction next?
|
||||
|
||||
xxx
|
||||
@@ -1,322 +1,322 @@
|
||||
# Tutorial 6: Shared State
|
||||
|
||||
Now that your components can communicate through events, the **sixth tutorial** introduces the next natural step in professional UI architecture: **shared state** (synchronizing multiple components via a central store).
|
||||
|
||||
You’ll learn how to implement a **lightweight reactive store** with Lit, integrate it with your existing `<hello-world>` and `<hello-dashboard>` components, and keep everything **test-driven**.
|
||||
|
||||
---
|
||||
|
||||
# ⚡ Tutorial 6 — Shared State
|
||||
|
||||
### “The Store Pattern” — Synchronizing Multiple Components
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goal
|
||||
|
||||
We’ll create a **store** that holds the global list of greeter names.
|
||||
All `<hello-world>` components will automatically update when the store changes.
|
||||
The `<hello-dashboard>` will use the store to add, remove, and rename greeters — without manually passing props or listening to custom events.
|
||||
|
||||
**Architecture overview:**
|
||||
|
||||
```
|
||||
+------------------------+
|
||||
| HelloDashboard |
|
||||
| (manages store ops) |
|
||||
| |
|
||||
| ┌────────────┐ |
|
||||
| | HelloWorld | <── store.subscribe()
|
||||
| └────────────┘ |
|
||||
| ┌────────────┐ |
|
||||
| | HelloWorld | |
|
||||
| └────────────┘ |
|
||||
+------------------------+
|
||||
│
|
||||
▼
|
||||
Shared Store
|
||||
(holds array of greeters)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 1. Step 1 — Write the failing test first
|
||||
|
||||
Create `src/store/hello-store.test.js`:
|
||||
|
||||
```javascript
|
||||
import { helloStore } from "./hello-store.js";
|
||||
|
||||
describe("helloStore", () => {
|
||||
it("starts with one default greeter", () => {
|
||||
const state = helloStore.getState();
|
||||
expect(state.greeters).to.deep.equal(["World"]);
|
||||
});
|
||||
|
||||
it("can add a new greeter", () => {
|
||||
helloStore.addGreeter("Alice");
|
||||
const state = helloStore.getState();
|
||||
expect(state.greeters).to.include("Alice");
|
||||
});
|
||||
|
||||
it("notifies subscribers on change", (done) => {
|
||||
const unsubscribe = helloStore.subscribe((state) => {
|
||||
expect(state.greeters).to.include("Bob");
|
||||
unsubscribe();
|
||||
done();
|
||||
});
|
||||
helloStore.addGreeter("Bob");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests fail (expected).
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 2. Step 2 — Implement the store
|
||||
|
||||
Create a new file:
|
||||
`src/store/hello-store.js`
|
||||
|
||||
```javascript
|
||||
class HelloStore {
|
||||
constructor() {
|
||||
this.state = { greeters: ["World"] };
|
||||
this.listeners = new Set();
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
_notify() {
|
||||
for (const cb of this.listeners) cb(this.state);
|
||||
}
|
||||
|
||||
subscribe(callback) {
|
||||
this.listeners.add(callback);
|
||||
callback(this.state); // immediate call with current state
|
||||
return () => this.listeners.delete(callback);
|
||||
}
|
||||
|
||||
addGreeter(name) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
greeters: [...this.state.greeters, name]
|
||||
};
|
||||
this._notify();
|
||||
}
|
||||
|
||||
renameGreeter(oldName, newName) {
|
||||
const updated = this.state.greeters.map((n) =>
|
||||
n === oldName ? newName : n
|
||||
);
|
||||
this.state = { ...this.state, greeters: updated };
|
||||
this._notify();
|
||||
}
|
||||
}
|
||||
|
||||
export const helloStore = new HelloStore();
|
||||
```
|
||||
|
||||
Re-run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ All tests should now pass.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 3. Step 3 — Update `<hello-dashboard>` to use the store
|
||||
|
||||
Modify `src/components/hello-dashboard/hello-dashboard.js`:
|
||||
|
||||
```javascript
|
||||
import { LitElement, html, css } from "lit";
|
||||
import "../hello-world/hello-world.js";
|
||||
import { helloStore } from "../../store/hello-store.js";
|
||||
|
||||
export class HelloDashboard extends LitElement {
|
||||
static styles = css`
|
||||
.container {
|
||||
font-family: system-ui, sans-serif;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.counter {
|
||||
margin: 0.5rem 0 1rem 0;
|
||||
font-weight: bold;
|
||||
color: #007acc;
|
||||
}
|
||||
button {
|
||||
background: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.greeters = [];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.unsubscribe = helloStore.subscribe((state) => {
|
||||
this.greeters = state.greeters;
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.unsubscribe?.();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
_addGreeter() {
|
||||
const newName = `User${this.greeters.length + 1}`;
|
||||
helloStore.addGreeter(newName);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<h2>Hello Dashboard</h2>
|
||||
<div class="counter">Total greetings: ${this.greeters.length}</div>
|
||||
|
||||
${this.greeters.map(
|
||||
(name) => html`<hello-world name=${name}></hello-world>`
|
||||
)}
|
||||
|
||||
<button @click=${this._addGreeter}>Add Greeting</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hello-dashboard", HelloDashboard);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧠 4. Step 4 — Update `<hello-world>` to use store for renaming
|
||||
|
||||
Update `_onInput()` in `src/components/hello-world/hello-world.js`:
|
||||
|
||||
```javascript
|
||||
import { helloStore } from "../../store/hello-store.js";
|
||||
|
||||
_onInput(event) {
|
||||
const oldName = this.name;
|
||||
this.name = event.target.value;
|
||||
helloStore.renameGreeter(oldName, this.name);
|
||||
}
|
||||
```
|
||||
|
||||
Now each greeter updates the shared store when its input changes — all other components subscribed to the store will react automatically.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 5. Step 5 — Integration test (optional)
|
||||
|
||||
Add `src/integration/dashboard-store.test.js`:
|
||||
|
||||
```javascript
|
||||
import "../components/hello-dashboard/hello-dashboard.js";
|
||||
import { helloStore } from "../store/hello-store.js";
|
||||
|
||||
describe("HelloDashboard and HelloStore integration", () => {
|
||||
it("renders as many greeters as store entries", async () => {
|
||||
helloStore.addGreeter("Eve");
|
||||
const el = document.createElement("hello-dashboard");
|
||||
document.body.appendChild(el);
|
||||
await el.updateComplete;
|
||||
|
||||
const greeters = el.shadowRoot.querySelectorAll("hello-world");
|
||||
expect(greeters.length).to.equal(helloStore.getState().greeters.length);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ All pass again.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 6. Step 6 — Try it live
|
||||
|
||||
Add to your `index.html`:
|
||||
|
||||
```html
|
||||
<hello-dashboard></hello-dashboard>
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
* Click “Add Greeting” → new greeters appear instantly.
|
||||
* Type into any greeter → all components referencing that name update automatically.
|
||||
|
||||
Congratulations — you’ve just implemented **a reactive state system** entirely in vanilla JS + Lit!
|
||||
|
||||
---
|
||||
|
||||
## 📘 7. Key Concepts
|
||||
|
||||
| Concept | Description |
|
||||
| -------------------------- | ----------------------------------------------------- |
|
||||
| **Centralized state** | One store manages the data for all components |
|
||||
| **Observer pattern** | Components subscribe to updates, re-render on change |
|
||||
| **Reactive sync** | Changes propagate automatically without prop drilling |
|
||||
| **TDD-first store design** | Ensures predictable, testable state behavior |
|
||||
| **Agent-friendliness** | Clear separation of state, presentation, and tests |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 8. Summary
|
||||
|
||||
At this point, your **TestDrive-UI** ecosystem includes:
|
||||
|
||||
| Layer | Responsibility |
|
||||
| ----------------- | ---------------------------- |
|
||||
| `hello-world` | Simple reactive component |
|
||||
| `hello-dashboard` | Container and coordinator |
|
||||
| `hello-store` | Shared, observable state |
|
||||
| Tests | Guard behavior at each level |
|
||||
|
||||
---
|
||||
|
||||
## 🔮 9. Next Tutorial (optional direction)
|
||||
|
||||
You can now evolve in one of two directions:
|
||||
|
||||
1. **Tutorial 7 — Persistence**
|
||||
→ Save and restore store state via `localStorage` or IndexedDB (so greetings persist).
|
||||
|
||||
2. **Tutorial 8 — Agent Automation**
|
||||
→ Introduce agent-driven TDD loops: use LLMs to generate new tests, detect regressions, and propose refactorings automatically.
|
||||
|
||||
---
|
||||
|
||||
Continue with **Tutorial 7: State Persistence** next — or move directly into **agent-driven TDD automation**.
|
||||
|
||||
xxx
|
||||
# Tutorial 6: Shared State
|
||||
|
||||
Now that your components can communicate through events, the **sixth tutorial** introduces the next natural step in professional UI architecture: **shared state** (synchronizing multiple components via a central store).
|
||||
|
||||
You’ll learn how to implement a **lightweight reactive store** with Lit, integrate it with your existing `<hello-world>` and `<hello-dashboard>` components, and keep everything **test-driven**.
|
||||
|
||||
---
|
||||
|
||||
# ⚡ Tutorial 6 — Shared State
|
||||
|
||||
### “The Store Pattern” — Synchronizing Multiple Components
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goal
|
||||
|
||||
We’ll create a **store** that holds the global list of greeter names.
|
||||
All `<hello-world>` components will automatically update when the store changes.
|
||||
The `<hello-dashboard>` will use the store to add, remove, and rename greeters — without manually passing props or listening to custom events.
|
||||
|
||||
**Architecture overview:**
|
||||
|
||||
```
|
||||
+------------------------+
|
||||
| HelloDashboard |
|
||||
| (manages store ops) |
|
||||
| |
|
||||
| ┌────────────┐ |
|
||||
| | HelloWorld | <── store.subscribe()
|
||||
| └────────────┘ |
|
||||
| ┌────────────┐ |
|
||||
| | HelloWorld | |
|
||||
| └────────────┘ |
|
||||
+------------------------+
|
||||
│
|
||||
▼
|
||||
Shared Store
|
||||
(holds array of greeters)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 1. Step 1 — Write the failing test first
|
||||
|
||||
Create `src/store/hello-store.test.js`:
|
||||
|
||||
```javascript
|
||||
import { helloStore } from "./hello-store.js";
|
||||
|
||||
describe("helloStore", () => {
|
||||
it("starts with one default greeter", () => {
|
||||
const state = helloStore.getState();
|
||||
expect(state.greeters).to.deep.equal(["World"]);
|
||||
});
|
||||
|
||||
it("can add a new greeter", () => {
|
||||
helloStore.addGreeter("Alice");
|
||||
const state = helloStore.getState();
|
||||
expect(state.greeters).to.include("Alice");
|
||||
});
|
||||
|
||||
it("notifies subscribers on change", (done) => {
|
||||
const unsubscribe = helloStore.subscribe((state) => {
|
||||
expect(state.greeters).to.include("Bob");
|
||||
unsubscribe();
|
||||
done();
|
||||
});
|
||||
helloStore.addGreeter("Bob");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests fail (expected).
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 2. Step 2 — Implement the store
|
||||
|
||||
Create a new file:
|
||||
`src/store/hello-store.js`
|
||||
|
||||
```javascript
|
||||
class HelloStore {
|
||||
constructor() {
|
||||
this.state = { greeters: ["World"] };
|
||||
this.listeners = new Set();
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
_notify() {
|
||||
for (const cb of this.listeners) cb(this.state);
|
||||
}
|
||||
|
||||
subscribe(callback) {
|
||||
this.listeners.add(callback);
|
||||
callback(this.state); // immediate call with current state
|
||||
return () => this.listeners.delete(callback);
|
||||
}
|
||||
|
||||
addGreeter(name) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
greeters: [...this.state.greeters, name]
|
||||
};
|
||||
this._notify();
|
||||
}
|
||||
|
||||
renameGreeter(oldName, newName) {
|
||||
const updated = this.state.greeters.map((n) =>
|
||||
n === oldName ? newName : n
|
||||
);
|
||||
this.state = { ...this.state, greeters: updated };
|
||||
this._notify();
|
||||
}
|
||||
}
|
||||
|
||||
export const helloStore = new HelloStore();
|
||||
```
|
||||
|
||||
Re-run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ All tests should now pass.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 3. Step 3 — Update `<hello-dashboard>` to use the store
|
||||
|
||||
Modify `src/components/hello-dashboard/hello-dashboard.js`:
|
||||
|
||||
```javascript
|
||||
import { LitElement, html, css } from "lit";
|
||||
import "../hello-world/hello-world.js";
|
||||
import { helloStore } from "../../store/hello-store.js";
|
||||
|
||||
export class HelloDashboard extends LitElement {
|
||||
static styles = css`
|
||||
.container {
|
||||
font-family: system-ui, sans-serif;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.counter {
|
||||
margin: 0.5rem 0 1rem 0;
|
||||
font-weight: bold;
|
||||
color: #007acc;
|
||||
}
|
||||
button {
|
||||
background: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.greeters = [];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.unsubscribe = helloStore.subscribe((state) => {
|
||||
this.greeters = state.greeters;
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.unsubscribe?.();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
_addGreeter() {
|
||||
const newName = `User${this.greeters.length + 1}`;
|
||||
helloStore.addGreeter(newName);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<h2>Hello Dashboard</h2>
|
||||
<div class="counter">Total greetings: ${this.greeters.length}</div>
|
||||
|
||||
${this.greeters.map(
|
||||
(name) => html`<hello-world name=${name}></hello-world>`
|
||||
)}
|
||||
|
||||
<button @click=${this._addGreeter}>Add Greeting</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hello-dashboard", HelloDashboard);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧠 4. Step 4 — Update `<hello-world>` to use store for renaming
|
||||
|
||||
Update `_onInput()` in `src/components/hello-world/hello-world.js`:
|
||||
|
||||
```javascript
|
||||
import { helloStore } from "../../store/hello-store.js";
|
||||
|
||||
_onInput(event) {
|
||||
const oldName = this.name;
|
||||
this.name = event.target.value;
|
||||
helloStore.renameGreeter(oldName, this.name);
|
||||
}
|
||||
```
|
||||
|
||||
Now each greeter updates the shared store when its input changes — all other components subscribed to the store will react automatically.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 5. Step 5 — Integration test (optional)
|
||||
|
||||
Add `src/integration/dashboard-store.test.js`:
|
||||
|
||||
```javascript
|
||||
import "../components/hello-dashboard/hello-dashboard.js";
|
||||
import { helloStore } from "../store/hello-store.js";
|
||||
|
||||
describe("HelloDashboard and HelloStore integration", () => {
|
||||
it("renders as many greeters as store entries", async () => {
|
||||
helloStore.addGreeter("Eve");
|
||||
const el = document.createElement("hello-dashboard");
|
||||
document.body.appendChild(el);
|
||||
await el.updateComplete;
|
||||
|
||||
const greeters = el.shadowRoot.querySelectorAll("hello-world");
|
||||
expect(greeters.length).to.equal(helloStore.getState().greeters.length);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
✅ All pass again.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 6. Step 6 — Try it live
|
||||
|
||||
Add to your `index.html`:
|
||||
|
||||
```html
|
||||
<hello-dashboard></hello-dashboard>
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
* Click “Add Greeting” → new greeters appear instantly.
|
||||
* Type into any greeter → all components referencing that name update automatically.
|
||||
|
||||
Congratulations — you’ve just implemented **a reactive state system** entirely in vanilla JS + Lit!
|
||||
|
||||
---
|
||||
|
||||
## 📘 7. Key Concepts
|
||||
|
||||
| Concept | Description |
|
||||
| -------------------------- | ----------------------------------------------------- |
|
||||
| **Centralized state** | One store manages the data for all components |
|
||||
| **Observer pattern** | Components subscribe to updates, re-render on change |
|
||||
| **Reactive sync** | Changes propagate automatically without prop drilling |
|
||||
| **TDD-first store design** | Ensures predictable, testable state behavior |
|
||||
| **Agent-friendliness** | Clear separation of state, presentation, and tests |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 8. Summary
|
||||
|
||||
At this point, your **TestDrive-UI** ecosystem includes:
|
||||
|
||||
| Layer | Responsibility |
|
||||
| ----------------- | ---------------------------- |
|
||||
| `hello-world` | Simple reactive component |
|
||||
| `hello-dashboard` | Container and coordinator |
|
||||
| `hello-store` | Shared, observable state |
|
||||
| Tests | Guard behavior at each level |
|
||||
|
||||
---
|
||||
|
||||
## 🔮 9. Next Tutorial (optional direction)
|
||||
|
||||
You can now evolve in one of two directions:
|
||||
|
||||
1. **Tutorial 7 — Persistence**
|
||||
→ Save and restore store state via `localStorage` or IndexedDB (so greetings persist).
|
||||
|
||||
2. **Tutorial 8 — Agent Automation**
|
||||
→ Introduce agent-driven TDD loops: use LLMs to generate new tests, detect regressions, and propose refactorings automatically.
|
||||
|
||||
---
|
||||
|
||||
Continue with **Tutorial 7: State Persistence** next — or move directly into **agent-driven TDD automation**.
|
||||
|
||||
xxx
|
||||
|
||||
@@ -1,238 +1,238 @@
|
||||
# Tutorial 7: Persistence Patterns
|
||||
|
||||
This tutorial teaches both **localStorage** and **IndexedDB** persistence patterns for your reactive store.
|
||||
|
||||
### “Saving Greetings” — Making the store survive reloads
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goal
|
||||
|
||||
Extend the `helloStore` so that all greetings persist between browser sessions.
|
||||
When the page reloads, previously added greeters are restored automatically.
|
||||
|
||||
We’ll implement this in **two steps**:
|
||||
|
||||
1. Start with simple **`localStorage`** persistence (fast, synchronous).
|
||||
2. Upgrade to **`IndexedDB`** for larger data or structured offline storage.
|
||||
|
||||
Both variants will keep your TestDrive-UI environment self-contained and testable.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Step 1 — Write the failing tests
|
||||
|
||||
Create a new file:
|
||||
`src/store/hello-store.persistence.test.js`
|
||||
|
||||
```javascript
|
||||
import { helloStore } from "./hello-store.js";
|
||||
|
||||
describe("helloStore persistence", () => {
|
||||
beforeEach(() => {
|
||||
// Clean simulated storage
|
||||
global.localStorage = {
|
||||
store: {},
|
||||
getItem(k) { return this.store[k] || null; },
|
||||
setItem(k, v) { this.store[k] = v; },
|
||||
clear() { this.store = {}; }
|
||||
};
|
||||
});
|
||||
|
||||
it("saves state to localStorage after modification", () => {
|
||||
helloStore.addGreeter("Persisty");
|
||||
const saved = JSON.parse(localStorage.getItem("helloStore"));
|
||||
expect(saved.greeters).to.include("Persisty");
|
||||
});
|
||||
|
||||
it("restores saved state when re-initialized", () => {
|
||||
localStorage.setItem(
|
||||
"helloStore",
|
||||
JSON.stringify({ greeters: ["Restored"] })
|
||||
);
|
||||
const { HelloStore } = await import("./hello-store.js");
|
||||
const freshStore = new HelloStore();
|
||||
const state = freshStore.getState();
|
||||
expect(state.greeters).to.deep.equal(["Restored"]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests will fail initially — perfect.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Step 2 — Implement `localStorage` persistence
|
||||
|
||||
Open `src/store/hello-store.js` and extend it like this:
|
||||
|
||||
```javascript
|
||||
class HelloStore {
|
||||
constructor() {
|
||||
const saved = localStorage.getItem("helloStore");
|
||||
this.state = saved ? JSON.parse(saved) : { greeters: ["World"] };
|
||||
this.listeners = new Set();
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
_notify() {
|
||||
// Save current state before notifying
|
||||
localStorage.setItem("helloStore", JSON.stringify(this.state));
|
||||
for (const cb of this.listeners) cb(this.state);
|
||||
}
|
||||
|
||||
subscribe(callback) {
|
||||
this.listeners.add(callback);
|
||||
callback(this.state);
|
||||
return () => this.listeners.delete(callback);
|
||||
}
|
||||
|
||||
addGreeter(name) {
|
||||
this.state = { ...this.state, greeters: [...this.state.greeters, name] };
|
||||
this._notify();
|
||||
}
|
||||
|
||||
renameGreeter(oldName, newName) {
|
||||
const updated = this.state.greeters.map((n) =>
|
||||
n === oldName ? newName : n
|
||||
);
|
||||
this.state = { ...this.state, greeters: updated };
|
||||
this._notify();
|
||||
}
|
||||
}
|
||||
|
||||
export const helloStore = new HelloStore();
|
||||
export { HelloStore }; // exported for testing
|
||||
```
|
||||
|
||||
✅ Run `npm test` again — tests should now pass.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Step 3 — Try it live
|
||||
|
||||
Start your dev server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
1. Add new greeters in the dashboard.
|
||||
2. Reload the browser.
|
||||
→ The same greeters reappear immediately!
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Step 4 — Optional: Switch to IndexedDB for larger data
|
||||
|
||||
`localStorage` is fine for small JSON objects.
|
||||
To persist more complex data or avoid blocking the main thread, you can use **IndexedDB** (via the tiny `idb-keyval` helper or native API).
|
||||
|
||||
Create `src/store/storage.js`:
|
||||
|
||||
```javascript
|
||||
// Minimal IndexedDB wrapper using idb-keyval style API
|
||||
const DB_NAME = "testdrive-ui";
|
||||
const STORE_NAME = "hello";
|
||||
|
||||
export async function saveState(state) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, 1);
|
||||
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME);
|
||||
req.onsuccess = () => {
|
||||
const tx = req.result.transaction(STORE_NAME, "readwrite");
|
||||
tx.objectStore(STORE_NAME).put(state, "store");
|
||||
tx.oncomplete = resolve;
|
||||
tx.onerror = reject;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadState() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, 1);
|
||||
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME);
|
||||
req.onsuccess = () => {
|
||||
const tx = req.result.transaction(STORE_NAME, "readonly");
|
||||
const get = tx.objectStore(STORE_NAME).get("store");
|
||||
get.onsuccess = () => resolve(get.result);
|
||||
get.onerror = reject;
|
||||
};
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Now update the store’s `_notify()` and constructor to use these async calls:
|
||||
|
||||
```javascript
|
||||
import { saveState, loadState } from "./storage.js";
|
||||
|
||||
class HelloStore {
|
||||
constructor() {
|
||||
this.listeners = new Set();
|
||||
loadState().then(
|
||||
(saved) =>
|
||||
(this.state = saved || { greeters: ["World"] }) &&
|
||||
this._notify()
|
||||
);
|
||||
}
|
||||
|
||||
async _notify() {
|
||||
await saveState(this.state);
|
||||
for (const cb of this.listeners) cb(this.state);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
💡 Tip: because IndexedDB operations are async, wrap state changes in `async` methods and adjust your tests with `await`.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Step 5 — Testing IndexedDB logic
|
||||
|
||||
In Node environments, you can simulate IndexedDB with `fake-indexeddb`:
|
||||
|
||||
```bash
|
||||
npm install --save-dev fake-indexeddb
|
||||
```
|
||||
|
||||
Then in your test setup:
|
||||
|
||||
```javascript
|
||||
import "fake-indexeddb/auto";
|
||||
```
|
||||
|
||||
Your tests now work without a browser.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Outcome
|
||||
|
||||
Your TestDrive-UI environment now supports:
|
||||
|
||||
| Persistence Layer | Use Case | Pros |
|
||||
| ----------------- | ------------------------------------- | ------------------ |
|
||||
| **localStorage** | Small, synchronous, quick persistence | Simple & immediate |
|
||||
| **IndexedDB** | Large, structured, async persistence | Scalable & robust |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Reflection
|
||||
|
||||
Persistence transforms your store into a **long-term memory layer**.
|
||||
With this, TestDrive-UI crosses into **progressive-web-app territory** — offline-ready, recoverable, and reliable.
|
||||
|
||||
---
|
||||
|
||||
Continue to Tutorial 8 on Agent Automation.
|
||||
|
||||
xxx
|
||||
# Tutorial 7: Persistence Patterns
|
||||
|
||||
This tutorial teaches both **localStorage** and **IndexedDB** persistence patterns for your reactive store.
|
||||
|
||||
### “Saving Greetings” — Making the store survive reloads
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goal
|
||||
|
||||
Extend the `helloStore` so that all greetings persist between browser sessions.
|
||||
When the page reloads, previously added greeters are restored automatically.
|
||||
|
||||
We’ll implement this in **two steps**:
|
||||
|
||||
1. Start with simple **`localStorage`** persistence (fast, synchronous).
|
||||
2. Upgrade to **`IndexedDB`** for larger data or structured offline storage.
|
||||
|
||||
Both variants will keep your TestDrive-UI environment self-contained and testable.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Step 1 — Write the failing tests
|
||||
|
||||
Create a new file:
|
||||
`src/store/hello-store.persistence.test.js`
|
||||
|
||||
```javascript
|
||||
import { helloStore } from "./hello-store.js";
|
||||
|
||||
describe("helloStore persistence", () => {
|
||||
beforeEach(() => {
|
||||
// Clean simulated storage
|
||||
global.localStorage = {
|
||||
store: {},
|
||||
getItem(k) { return this.store[k] || null; },
|
||||
setItem(k, v) { this.store[k] = v; },
|
||||
clear() { this.store = {}; }
|
||||
};
|
||||
});
|
||||
|
||||
it("saves state to localStorage after modification", () => {
|
||||
helloStore.addGreeter("Persisty");
|
||||
const saved = JSON.parse(localStorage.getItem("helloStore"));
|
||||
expect(saved.greeters).to.include("Persisty");
|
||||
});
|
||||
|
||||
it("restores saved state when re-initialized", () => {
|
||||
localStorage.setItem(
|
||||
"helloStore",
|
||||
JSON.stringify({ greeters: ["Restored"] })
|
||||
);
|
||||
const { HelloStore } = await import("./hello-store.js");
|
||||
const freshStore = new HelloStore();
|
||||
const state = freshStore.getState();
|
||||
expect(state.greeters).to.deep.equal(["Restored"]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All tests will fail initially — perfect.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Step 2 — Implement `localStorage` persistence
|
||||
|
||||
Open `src/store/hello-store.js` and extend it like this:
|
||||
|
||||
```javascript
|
||||
class HelloStore {
|
||||
constructor() {
|
||||
const saved = localStorage.getItem("helloStore");
|
||||
this.state = saved ? JSON.parse(saved) : { greeters: ["World"] };
|
||||
this.listeners = new Set();
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
_notify() {
|
||||
// Save current state before notifying
|
||||
localStorage.setItem("helloStore", JSON.stringify(this.state));
|
||||
for (const cb of this.listeners) cb(this.state);
|
||||
}
|
||||
|
||||
subscribe(callback) {
|
||||
this.listeners.add(callback);
|
||||
callback(this.state);
|
||||
return () => this.listeners.delete(callback);
|
||||
}
|
||||
|
||||
addGreeter(name) {
|
||||
this.state = { ...this.state, greeters: [...this.state.greeters, name] };
|
||||
this._notify();
|
||||
}
|
||||
|
||||
renameGreeter(oldName, newName) {
|
||||
const updated = this.state.greeters.map((n) =>
|
||||
n === oldName ? newName : n
|
||||
);
|
||||
this.state = { ...this.state, greeters: updated };
|
||||
this._notify();
|
||||
}
|
||||
}
|
||||
|
||||
export const helloStore = new HelloStore();
|
||||
export { HelloStore }; // exported for testing
|
||||
```
|
||||
|
||||
✅ Run `npm test` again — tests should now pass.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Step 3 — Try it live
|
||||
|
||||
Start your dev server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
1. Add new greeters in the dashboard.
|
||||
2. Reload the browser.
|
||||
→ The same greeters reappear immediately!
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Step 4 — Optional: Switch to IndexedDB for larger data
|
||||
|
||||
`localStorage` is fine for small JSON objects.
|
||||
To persist more complex data or avoid blocking the main thread, you can use **IndexedDB** (via the tiny `idb-keyval` helper or native API).
|
||||
|
||||
Create `src/store/storage.js`:
|
||||
|
||||
```javascript
|
||||
// Minimal IndexedDB wrapper using idb-keyval style API
|
||||
const DB_NAME = "testdrive-ui";
|
||||
const STORE_NAME = "hello";
|
||||
|
||||
export async function saveState(state) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, 1);
|
||||
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME);
|
||||
req.onsuccess = () => {
|
||||
const tx = req.result.transaction(STORE_NAME, "readwrite");
|
||||
tx.objectStore(STORE_NAME).put(state, "store");
|
||||
tx.oncomplete = resolve;
|
||||
tx.onerror = reject;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadState() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, 1);
|
||||
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME);
|
||||
req.onsuccess = () => {
|
||||
const tx = req.result.transaction(STORE_NAME, "readonly");
|
||||
const get = tx.objectStore(STORE_NAME).get("store");
|
||||
get.onsuccess = () => resolve(get.result);
|
||||
get.onerror = reject;
|
||||
};
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Now update the store’s `_notify()` and constructor to use these async calls:
|
||||
|
||||
```javascript
|
||||
import { saveState, loadState } from "./storage.js";
|
||||
|
||||
class HelloStore {
|
||||
constructor() {
|
||||
this.listeners = new Set();
|
||||
loadState().then(
|
||||
(saved) =>
|
||||
(this.state = saved || { greeters: ["World"] }) &&
|
||||
this._notify()
|
||||
);
|
||||
}
|
||||
|
||||
async _notify() {
|
||||
await saveState(this.state);
|
||||
for (const cb of this.listeners) cb(this.state);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
💡 Tip: because IndexedDB operations are async, wrap state changes in `async` methods and adjust your tests with `await`.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Step 5 — Testing IndexedDB logic
|
||||
|
||||
In Node environments, you can simulate IndexedDB with `fake-indexeddb`:
|
||||
|
||||
```bash
|
||||
npm install --save-dev fake-indexeddb
|
||||
```
|
||||
|
||||
Then in your test setup:
|
||||
|
||||
```javascript
|
||||
import "fake-indexeddb/auto";
|
||||
```
|
||||
|
||||
Your tests now work without a browser.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Outcome
|
||||
|
||||
Your TestDrive-UI environment now supports:
|
||||
|
||||
| Persistence Layer | Use Case | Pros |
|
||||
| ----------------- | ------------------------------------- | ------------------ |
|
||||
| **localStorage** | Small, synchronous, quick persistence | Simple & immediate |
|
||||
| **IndexedDB** | Large, structured, async persistence | Scalable & robust |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Reflection
|
||||
|
||||
Persistence transforms your store into a **long-term memory layer**.
|
||||
With this, TestDrive-UI crosses into **progressive-web-app territory** — offline-ready, recoverable, and reliable.
|
||||
|
||||
---
|
||||
|
||||
Continue to Tutorial 8 on Agent Automation.
|
||||
|
||||
xxx
|
||||
|
||||
@@ -1,242 +1,242 @@
|
||||
# 🤖 Tutorial 8 — Agent-Driven TDD Automation
|
||||
|
||||
This tutorial focuses on *Agent-Driven TDD Automation* — turning TestDrive-UI into a living collaboration space between human developers and coding agents.
|
||||
|
||||
### “Coding with Companions” — Using AI Agents for Continuous Improvement
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goal
|
||||
|
||||
Integrate LLM-based coding agents into your TestDrive-UI workflow so they can:
|
||||
|
||||
1. **Generate new tests** from natural-language requirements.
|
||||
2. **Run tests and detect regressions** automatically.
|
||||
3. **Propose targeted refactorings or patches** when failures occur.
|
||||
|
||||
By combining deterministic testing with creative reasoning, you build a feedback loop that never stops improving your code.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Concept Overview
|
||||
|
||||
Traditional TDD cycle:
|
||||
|
||||
```
|
||||
requirement → test → fail → code → pass → refactor
|
||||
```
|
||||
|
||||
Agent-Driven TDD cycle:
|
||||
|
||||
```
|
||||
requirement → agent generates test → run → fail →
|
||||
agent proposes fix → run → pass → review → merge
|
||||
```
|
||||
|
||||
You remain the architect and reviewer — the agent acts as an automated junior developer executing fast, repeatable loops.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Step 1 — Define a Machine-Readable Requirement
|
||||
|
||||
Create a simple JSON file to store behavioral specifications.
|
||||
|
||||
`specs/hello-world.name-change.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"component": "hello-world",
|
||||
"feature": "name-change",
|
||||
"description": "Greeting text should update when the user types a new name.",
|
||||
"expected_behavior": [
|
||||
"Typing in the input field updates the displayed greeting immediately.",
|
||||
"The component property 'name' reflects the latest input value."
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Agents use these files as prompts to generate corresponding tests.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Step 2 — Agent Generates a Test
|
||||
|
||||
An agent reads the JSON spec and produces Mocha tests automatically.
|
||||
|
||||
Example output from an LLM agent:
|
||||
|
||||
`generated-tests/hello-world.name-change.test.js`
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
describe("<hello-world> (auto-generated name-change)", () => {
|
||||
it("updates greeting text when user types", async () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
const input = el.shadowRoot.querySelector("input");
|
||||
input.value = "Agent";
|
||||
input.dispatchEvent(new Event("input"));
|
||||
await el.updateComplete;
|
||||
const greeting = el.shadowRoot.querySelector(".greeting");
|
||||
expect(greeting.textContent.trim()).to.equal("Hello, Agent!");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run your full suite:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
If the test fails, the agent analyzes output and proposes a minimal fix for `hello-world.js`.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Step 3 — Detect Regressions
|
||||
|
||||
Agents monitor test results over time by parsing Mocha’s machine-readable report (`--reporter json`).
|
||||
|
||||
Example agent logic (pseudocode):
|
||||
|
||||
```python
|
||||
def analyze_report(report):
|
||||
failed = [t for t in report['tests'] if t['err']]
|
||||
if not failed:
|
||||
return "All green!"
|
||||
for f in failed:
|
||||
print(f"Regression in {f['title']}: {f['err']['message']}")
|
||||
propose_fix(f)
|
||||
```
|
||||
|
||||
This analysis lets agents flag new failures, auto-create issues, or propose PRs.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Step 4 — Agent Proposes Refactorings
|
||||
|
||||
Once tests are green, agents can examine code for quality signals:
|
||||
|
||||
| Heuristic | Example Action |
|
||||
| --------------------- | ------------------------------------------ |
|
||||
| Duplicate logic | Extract shared helper or controller |
|
||||
| Excessive DOM queries | Cache references or use ReactiveController |
|
||||
| Long methods | Suggest method splitting |
|
||||
| Repeated strings | Propose localization constants |
|
||||
|
||||
Example prompt to your agent:
|
||||
|
||||
```
|
||||
“Review src/components/hello-world.js and propose a refactor
|
||||
that reduces duplication without breaking current tests.”
|
||||
```
|
||||
|
||||
The agent runs tests after each change to validate its proposal.
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Step 5 — Automate the Loop
|
||||
|
||||
Create a simple Node script to chain the whole process.
|
||||
|
||||
`scripts/agent-tdd.js`
|
||||
|
||||
```javascript
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import OpenAI from "openai"; // or another LLM SDK
|
||||
|
||||
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
|
||||
function runTests() {
|
||||
try {
|
||||
const output = execSync("npx mocha --reporter json", { encoding: "utf-8" });
|
||||
return JSON.parse(output);
|
||||
} catch (e) {
|
||||
return JSON.parse(e.stdout);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const report = runTests();
|
||||
const failed = report.tests.filter(t => t.err.message);
|
||||
if (failed.length === 0) return console.log("✅ All tests passed");
|
||||
|
||||
const prompt = `Some tests failed:\n${JSON.stringify(failed, null, 2)}\n
|
||||
Propose minimal code changes to fix them.`;
|
||||
const completion = await client.responses.create({ model: "gpt-5", input: prompt });
|
||||
fs.writeFileSync("agent-proposal.txt", completion.output_text);
|
||||
console.log("💡 Agent proposal written to agent-proposal.txt");
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
This script connects your tests with an AI assistant that learns and suggests fixes continuously.
|
||||
|
||||
---
|
||||
|
||||
## 🧰 Step 6 — Integrate into CI
|
||||
|
||||
Add a CI job (e.g., GitHub Actions or local cron) to run the agent loop daily or on push.
|
||||
|
||||
Example workflow:
|
||||
|
||||
```
|
||||
on:
|
||||
push:
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
jobs:
|
||||
agent-tdd:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: npm ci
|
||||
- run: node scripts/agent-tdd.js
|
||||
```
|
||||
|
||||
Now your project self-tests and self-critiques even when you’re offline.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Step 7 — Visualize Agent Progress
|
||||
|
||||
Agents can log progress into a dashboard component (`<agent-console>`) showing:
|
||||
|
||||
* Number of tests generated.
|
||||
* Pass/fail trend over time.
|
||||
* Proposed vs. accepted refactors.
|
||||
|
||||
It’s your window into the machine’s learning curve.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Outcome
|
||||
|
||||
You now have a self-reinforcing loop:
|
||||
|
||||
1. Humans write specs.
|
||||
2. Agents create tests and code.
|
||||
3. The suite proves stability.
|
||||
4. Agents refactor and review under guard of tests.
|
||||
|
||||
This combines the discipline of TDD with the creativity and endurance of AI.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Next Steps
|
||||
|
||||
* Add **semantic diff filters** so agents learn from accepted patches.
|
||||
* Train agents to cluster tests into *feature domains* for smarter coverage analysis.
|
||||
* Integrate Storybook snapshots for visual regression detection.
|
||||
* Build a CLI (`npx agent-tdd`) to run and audit your AI test loops interactively.
|
||||
|
||||
---
|
||||
|
||||
Congratulations! You finished all tutorials and should be fine going Forward Building components.
|
||||
|
||||
Feel free to tell us which additional tutorials we should provide.
|
||||
|
||||
# 🤖 Tutorial 8 — Agent-Driven TDD Automation
|
||||
|
||||
This tutorial focuses on *Agent-Driven TDD Automation* — turning TestDrive-UI into a living collaboration space between human developers and coding agents.
|
||||
|
||||
### “Coding with Companions” — Using AI Agents for Continuous Improvement
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goal
|
||||
|
||||
Integrate LLM-based coding agents into your TestDrive-UI workflow so they can:
|
||||
|
||||
1. **Generate new tests** from natural-language requirements.
|
||||
2. **Run tests and detect regressions** automatically.
|
||||
3. **Propose targeted refactorings or patches** when failures occur.
|
||||
|
||||
By combining deterministic testing with creative reasoning, you build a feedback loop that never stops improving your code.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Concept Overview
|
||||
|
||||
Traditional TDD cycle:
|
||||
|
||||
```
|
||||
requirement → test → fail → code → pass → refactor
|
||||
```
|
||||
|
||||
Agent-Driven TDD cycle:
|
||||
|
||||
```
|
||||
requirement → agent generates test → run → fail →
|
||||
agent proposes fix → run → pass → review → merge
|
||||
```
|
||||
|
||||
You remain the architect and reviewer — the agent acts as an automated junior developer executing fast, repeatable loops.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Step 1 — Define a Machine-Readable Requirement
|
||||
|
||||
Create a simple JSON file to store behavioral specifications.
|
||||
|
||||
`specs/hello-world.name-change.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"component": "hello-world",
|
||||
"feature": "name-change",
|
||||
"description": "Greeting text should update when the user types a new name.",
|
||||
"expected_behavior": [
|
||||
"Typing in the input field updates the displayed greeting immediately.",
|
||||
"The component property 'name' reflects the latest input value."
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Agents use these files as prompts to generate corresponding tests.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Step 2 — Agent Generates a Test
|
||||
|
||||
An agent reads the JSON spec and produces Mocha tests automatically.
|
||||
|
||||
Example output from an LLM agent:
|
||||
|
||||
`generated-tests/hello-world.name-change.test.js`
|
||||
|
||||
```javascript
|
||||
import "./hello-world.js";
|
||||
|
||||
describe("<hello-world> (auto-generated name-change)", () => {
|
||||
it("updates greeting text when user types", async () => {
|
||||
const el = document.createElement("hello-world");
|
||||
document.body.appendChild(el);
|
||||
const input = el.shadowRoot.querySelector("input");
|
||||
input.value = "Agent";
|
||||
input.dispatchEvent(new Event("input"));
|
||||
await el.updateComplete;
|
||||
const greeting = el.shadowRoot.querySelector(".greeting");
|
||||
expect(greeting.textContent.trim()).to.equal("Hello, Agent!");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run your full suite:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
If the test fails, the agent analyzes output and proposes a minimal fix for `hello-world.js`.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Step 3 — Detect Regressions
|
||||
|
||||
Agents monitor test results over time by parsing Mocha’s machine-readable report (`--reporter json`).
|
||||
|
||||
Example agent logic (pseudocode):
|
||||
|
||||
```python
|
||||
def analyze_report(report):
|
||||
failed = [t for t in report['tests'] if t['err']]
|
||||
if not failed:
|
||||
return "All green!"
|
||||
for f in failed:
|
||||
print(f"Regression in {f['title']}: {f['err']['message']}")
|
||||
propose_fix(f)
|
||||
```
|
||||
|
||||
This analysis lets agents flag new failures, auto-create issues, or propose PRs.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Step 4 — Agent Proposes Refactorings
|
||||
|
||||
Once tests are green, agents can examine code for quality signals:
|
||||
|
||||
| Heuristic | Example Action |
|
||||
| --------------------- | ------------------------------------------ |
|
||||
| Duplicate logic | Extract shared helper or controller |
|
||||
| Excessive DOM queries | Cache references or use ReactiveController |
|
||||
| Long methods | Suggest method splitting |
|
||||
| Repeated strings | Propose localization constants |
|
||||
|
||||
Example prompt to your agent:
|
||||
|
||||
```
|
||||
“Review src/components/hello-world.js and propose a refactor
|
||||
that reduces duplication without breaking current tests.”
|
||||
```
|
||||
|
||||
The agent runs tests after each change to validate its proposal.
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Step 5 — Automate the Loop
|
||||
|
||||
Create a simple Node script to chain the whole process.
|
||||
|
||||
`scripts/agent-tdd.js`
|
||||
|
||||
```javascript
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import OpenAI from "openai"; // or another LLM SDK
|
||||
|
||||
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
|
||||
function runTests() {
|
||||
try {
|
||||
const output = execSync("npx mocha --reporter json", { encoding: "utf-8" });
|
||||
return JSON.parse(output);
|
||||
} catch (e) {
|
||||
return JSON.parse(e.stdout);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const report = runTests();
|
||||
const failed = report.tests.filter(t => t.err.message);
|
||||
if (failed.length === 0) return console.log("✅ All tests passed");
|
||||
|
||||
const prompt = `Some tests failed:\n${JSON.stringify(failed, null, 2)}\n
|
||||
Propose minimal code changes to fix them.`;
|
||||
const completion = await client.responses.create({ model: "gpt-5", input: prompt });
|
||||
fs.writeFileSync("agent-proposal.txt", completion.output_text);
|
||||
console.log("💡 Agent proposal written to agent-proposal.txt");
|
||||
}
|
||||
|
||||
main();
|
||||
```
|
||||
|
||||
This script connects your tests with an AI assistant that learns and suggests fixes continuously.
|
||||
|
||||
---
|
||||
|
||||
## 🧰 Step 6 — Integrate into CI
|
||||
|
||||
Add a CI job (e.g., GitHub Actions or local cron) to run the agent loop daily or on push.
|
||||
|
||||
Example workflow:
|
||||
|
||||
```
|
||||
on:
|
||||
push:
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
jobs:
|
||||
agent-tdd:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: npm ci
|
||||
- run: node scripts/agent-tdd.js
|
||||
```
|
||||
|
||||
Now your project self-tests and self-critiques even when you’re offline.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Step 7 — Visualize Agent Progress
|
||||
|
||||
Agents can log progress into a dashboard component (`<agent-console>`) showing:
|
||||
|
||||
* Number of tests generated.
|
||||
* Pass/fail trend over time.
|
||||
* Proposed vs. accepted refactors.
|
||||
|
||||
It’s your window into the machine’s learning curve.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Outcome
|
||||
|
||||
You now have a self-reinforcing loop:
|
||||
|
||||
1. Humans write specs.
|
||||
2. Agents create tests and code.
|
||||
3. The suite proves stability.
|
||||
4. Agents refactor and review under guard of tests.
|
||||
|
||||
This combines the discipline of TDD with the creativity and endurance of AI.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Next Steps
|
||||
|
||||
* Add **semantic diff filters** so agents learn from accepted patches.
|
||||
* Train agents to cluster tests into *feature domains* for smarter coverage analysis.
|
||||
* Integrate Storybook snapshots for visual regression detection.
|
||||
* Build a CLI (`npx agent-tdd`) to run and audit your AI test loops interactively.
|
||||
|
||||
---
|
||||
|
||||
Congratulations! You finished all tutorials and should be fine going Forward Building components.
|
||||
|
||||
Feel free to tell us which additional tutorials we should provide.
|
||||
|
||||
xxx
|
||||
Reference in New Issue
Block a user