doc: Tutorials for new users

This commit is contained in:
2025-11-03 21:36:45 +01:00
parent 9fbd10ffbc
commit 03656a1f19
9 changed files with 2080 additions and 0 deletions

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