generated from coulomb/repo-seed
doc: Tutorials for new users
This commit is contained in:
144
tutorials/Additional Tutorial Suggestions.md
Normal file
144
tutorials/Additional Tutorial Suggestions.md
Normal file
@@ -0,0 +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
|
||||||
187
tutorials/Tutorial 1 Hello World.md
Normal file
187
tutorials/Tutorial 1 Hello World.md
Normal file
@@ -0,0 +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
|
||||||
217
tutorials/Tutorial 2 Reactive Properties.md
Normal file
217
tutorials/Tutorial 2 Reactive Properties.md
Normal file
@@ -0,0 +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.
|
||||||
|
|
||||||
|
xxx
|
||||||
221
tutorials/Tutorial 3 Bidirectional Data Binding.md
Normal file
221
tutorials/Tutorial 3 Bidirectional Data Binding.md
Normal file
@@ -0,0 +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.
|
||||||
|
|
||||||
|
xxx
|
||||||
257
tutorials/Tutorial 4 Component Composition.md
Normal file
257
tutorials/Tutorial 4 Component Composition.md
Normal file
@@ -0,0 +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
|
||||||
252
tutorials/Tutorial 5 Component Communication.md
Normal file
252
tutorials/Tutorial 5 Component Communication.md
Normal file
@@ -0,0 +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?
|
||||||
|
|
||||||
|
xxx
|
||||||
322
tutorials/Tutorial 6 Shared State.md
Normal file
322
tutorials/Tutorial 6 Shared State.md
Normal file
@@ -0,0 +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
|
||||||
238
tutorials/Tutorial 7 Persistence Patterns.md
Normal file
238
tutorials/Tutorial 7 Persistence Patterns.md
Normal file
@@ -0,0 +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
|
||||||
242
tutorials/Tutorial 8 Agent Driven Tdd Automation.md
Normal file
242
tutorials/Tutorial 8 Agent Driven Tdd Automation.md
Normal file
@@ -0,0 +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.
|
||||||
|
|
||||||
|
xxx
|
||||||
Reference in New Issue
Block a user