test(e2e): les parcours applicatifs passent par l'app d'exemple
Une seconde suite e2e, `packages/sdk/e2e/notebook.ts` (`test:e2e:app`), qui pilote `examples/notebook` par le DOM — une page de navigateur par identité — contre le même broker réel. **Pourquoi une seconde suite plutôt qu'un ajout dans la première.** `run.ts` parle à un sac de méthodes sur `window.__sdk` : cela prouve que les fonctions TOURNENT, jamais qu'une application peut s'écrire avec. L'écart a déjà coûté un défaut livré — l'inbox d'un document était verte ici et inutilisable en pratique, parce que le harnais pouvait faire traverser une adresse d'une identité à l'autre par une variable, canal qu'aucune application n'a. Ici, rien ne traverse que ce qui traverse dans la vie : la RÉFÉRENCE d'une note, recopiée d'un écran, et un identifiant tapé dans un champ. Quatre parcours, qui se lisent comme des parcours : - Bob lit la note publique d'Alice depuis sa seule référence — la propriété pour laquelle l'émulation du store public existe, vérifiée bout en bout et sans qu'aucune clé ne circule ; - la note protégée d'Alice reste fermée jusqu'à ce qu'elle la partage — même geste côté Bob, issue opposée, décidée par où la note se trouve ; - Bob laisse un message sur la note d'Alice, et seule Alice le lit — il TROUVE l'adresse depuis la note, personne ne la lui donne ; - la liste de chacun ne contient que ses notes. **Deux étapes quittent `run.ts`** (`documentInboxDeposit`, `capsShareCap`), avec un commentaire disant où elles sont parties : ce sont des parcours, et ils valent plus joués sur deux écrans que sur deux appels d'une même page. Ce qui reste là-bas est ce qu'une application ne fait pas : primitives, caractérisation, régressions de démarrage à froid. **Trois défauts trouvés en écrivant la suite**, tous côté application et invisibles pour le harnais : `connectedUser()` devait être attendu à la connexion (sinon une note qu'on vient de vous partager se lit comme illisible — ce qui ressemble à un problème de droits alors que c'est un problème de moment) ; une réponse périmée restait affichée à côté d'une question fraîche ; et changer de portée ne rafraîchissait pas la liste. L'app affiche désormais la référence de chaque note — ce qu'aucun écran ne montre, aucun utilisateur ne peut le faire circuler. Corrigé au passage : le `.gitignore` pointait encore `packages/client/`, si bien que le commit de renommage a embarqué le profil Playwright de la suite e2e (226 fichiers). Les chemins sont réalignés et le commit précédent a été amendé — rien n'était poussé. 179 tests unitaires, e2e 40/40 (3,2 min) et applicatif 10/10 (0,7 min).
This commit is contained in:
@@ -6,14 +6,25 @@ It exists for two reasons, and the second is the one that matters.
|
||||
|
||||
**It shows how to use the library.** Every call in `app.ts` is what a real consumer writes. There is no test scaffolding, no privileged import, no reaching into the library's internals — it resolves `@ng-eventually/sdk` as an external consumer does. If something reads awkwardly here, it reads awkwardly for everyone.
|
||||
|
||||
**It is what the e2e suite is meant to drive** — and does not yet, which this line says out loud rather than implying otherwise. The suite still talks to a bag of methods on `window.__sdk` (`packages/sdk/e2e/sdk-entry.ts`), which proves the functions run but never that an application can be written with them. That gap shipped a real defect once: a document's inbox was green in tests and unusable in practice, because the harness handed an address across an identity boundary through a variable — something no application can do. This app can only do what an application can do, which is why the applicative scenarios belong here.
|
||||
**It is what the applicative e2e suite drives** (`packages/sdk/e2e/notebook.ts`, `bun run test:e2e:app`). The other suite talks to a bag of methods on `window.__sdk`, which proves the functions run but never that an application can be written with them — and that gap shipped a real defect once: a document's inbox was green in tests and unusable in practice, because the harness handed an address across an identity boundary through a variable, something no application can do. Here each identity is its own browser page, and the only values that cross between them are the ones that cross in life: a note's reference, copied off one screen, and an identifier typed into a field.
|
||||
|
||||
It has already paid for itself twice: writing it surfaced that `UnionSubject` returned `string` where the values are always document references (so a consumer had to cast whatever it had just read before passing it back), and that the access gate normalized what a user typed but not what the URL carried.
|
||||
It has already paid for itself: writing it surfaced that `UnionSubject` returned `string` where the values are always document references (so a consumer had to cast whatever it had just read before passing it back), and that the access gate normalized what a user typed but not what the URL carried.
|
||||
|
||||
## What it exercises
|
||||
|
||||
Signing in, writing notes by scope, listing one's own, reading a note received as a link, handing a reader the key to a private note, opening a note for messages, leaving a message on someone else's note, and reacting to changes.
|
||||
Signing in, writing notes by scope, listing one's own, reading a note from its bare reference, handing a reader the key to a protected note, opening a note for messages, leaving a message on someone else's note, and reacting to changes.
|
||||
|
||||
The four journeys the suite runs, and what each proves:
|
||||
|
||||
| Journey | What it proves |
|
||||
|---|---|
|
||||
| Bob reads Alice's PUBLIC note from its reference alone | A public store serves its notes' keys — a bare reference is enough, and no key ever crosses |
|
||||
| Alice's PROTECTED note stays shut until she shares it | The same gesture, the opposite outcome, decided by where the note sits and not by what was sent |
|
||||
| Bob leaves a message on Alice's note, and only Alice reads it | A depositor FINDS the address from the note itself; depositing grants no reading |
|
||||
| Each actor's list holds their own notes | The boundary, seen from the only place that matters: the screen |
|
||||
|
||||
It has also found three defects of its own, each an application-side one the harness could not see: `connectedUser()` had to be awaited at sign-in (a note just shared with you reads as unreadable otherwise), a stale answer stayed on screen beside a fresh question, and changing the scope did not refresh the list.
|
||||
|
||||
## Running it
|
||||
|
||||
The e2e suite builds and serves it (`packages/sdk/e2e/`). To open it by hand you need a wallet: serve the folder with a bundled `app.js` and a `/shared-wallet.ngw`, and set `__NOTEBOOK_WALLET_PASSWORD__`.
|
||||
`cd packages/sdk && bun run test:e2e:app` builds it, serves it, and drives it against the real broker. To open it by hand you need a wallet: serve the folder with a bundled `app.js` and a `/shared-wallet.ngw`, and set `__NOTEBOOK_WALLET_PASSWORD__`.
|
||||
|
||||
+39
-16
@@ -7,13 +7,14 @@
|
||||
* writes; there is no test scaffolding, no privileged import, no reaching into the
|
||||
* library's internals. If something is awkward here, it is awkward for everyone.
|
||||
*
|
||||
* 2. **It is what the e2e suite drives.** The suite used to talk to a bag of methods on
|
||||
* `window.__sdk`, which proved the functions ran but never that an application could
|
||||
* be written with them — and that gap shipped a real defect: a document's inbox was
|
||||
* green in tests and unusable in practice, because the harness handed the address
|
||||
* across an identity boundary through a variable. No application can do that. This
|
||||
* app can only do what an application can do, so a test that passes here means the
|
||||
* surface is usable, not merely callable.
|
||||
* 2. **It is what the applicative e2e suite drives** (`packages/sdk/e2e/notebook.ts`).
|
||||
* The other suite talks to a bag of methods on `window.__sdk`, which proves the
|
||||
* functions run but never that an application could be written with them — and that
|
||||
* gap shipped a real defect: a document's inbox was green in tests and unusable in
|
||||
* practice, because the harness handed the address across an identity boundary
|
||||
* through a variable. No application can do that. This app can only do what an
|
||||
* application can do, so a test that passes here means the surface is usable, not
|
||||
* merely callable.
|
||||
*
|
||||
* ── The domain is deliberately thin ───────────────────────────────────────
|
||||
* Notes. Each user writes their own, may publish one, may hand a reader the key to a
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
readUnion,
|
||||
storeRegistry,
|
||||
subscribeDoc,
|
||||
connectedUser,
|
||||
type Nuri,
|
||||
type Scope,
|
||||
// Polyfill-era — these three go away, and they are the whole of what goes away.
|
||||
@@ -121,15 +123,20 @@ async function myNotes(scope: Scope): Promise<Note[]> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read someone else's note from its link.
|
||||
* Read someone else's note from its REFERENCE.
|
||||
*
|
||||
* The link is what circulates in this model — you do not discover a note, you are given
|
||||
* its link. It arrives as a plain string, from a field or a URL, and goes straight in:
|
||||
* the library validates it. Nothing to narrow, nothing to cast, and nothing that will
|
||||
* have to change when the real SDK takes that same string.
|
||||
* A reference is what circulates — you do not discover a note, someone gives you its
|
||||
* reference (a message, a URL, a QR code). It carries no key, and that is the point:
|
||||
* if the note is in a PUBLIC store the store hands its key to whoever asks, so the
|
||||
* reference is enough; if it is protected, the reference names the note and opens
|
||||
* nothing, until its owner shares it (see `shareNote`).
|
||||
*
|
||||
* It arrives as a plain string, from a field or a URL, and goes straight in: the
|
||||
* library validates it. Nothing to narrow, nothing to cast, and nothing that will have
|
||||
* to change when the real SDK takes that same string.
|
||||
*/
|
||||
async function readSharedNote(link: string): Promise<Note | null> {
|
||||
const [note] = await readUnion([link]);
|
||||
async function readSharedNote(reference: string): Promise<Note | null> {
|
||||
const [note] = await readUnion([reference]);
|
||||
if (!note) return null;
|
||||
return {
|
||||
doc: note.subject,
|
||||
@@ -182,11 +189,19 @@ function currentIdentity(): string {
|
||||
/**
|
||||
* Sign in. The library shows its access barrier when it needs one; the day the wallet
|
||||
* supplies the identity, this resolves silently and nothing here changes.
|
||||
*
|
||||
* The `connectedUser()` await is not optional decoration, and the applicative e2e is
|
||||
* what found that out: setting an identity FIRES the connection work — restoring what
|
||||
* others shared with you, draining your inboxes — but does not wait for it. Render
|
||||
* before it lands and a note someone just shared reads as unreadable, which looks like
|
||||
* a permission problem and is a timing one. At migration this becomes the session
|
||||
* opening, and the await stays exactly where it is.
|
||||
*/
|
||||
async function signIn(): Promise<void> {
|
||||
await ensureIdentity();
|
||||
await sessionReady;
|
||||
identity = readIdentityBack();
|
||||
await connectedUser();
|
||||
}
|
||||
|
||||
/** The library owns the identity; the app asks for it rather than remembering it. */
|
||||
@@ -213,6 +228,7 @@ async function refresh(): Promise<void> {
|
||||
<button class="share" data-doc="${n.doc}">partager</button>
|
||||
<button class="open" data-doc="${n.doc}">ouvrir aux messages</button>
|
||||
<button class="msgs" data-doc="${n.doc}">messages</button>
|
||||
<div><code class="ref" data-testid="ref">${n.doc}</code></div>
|
||||
</li>`,
|
||||
)
|
||||
.join("");
|
||||
@@ -220,15 +236,20 @@ async function refresh(): Promise<void> {
|
||||
}
|
||||
|
||||
function wire(): void {
|
||||
// Changing the scope changes which notes are listed — without this the list keeps
|
||||
// showing the previous scope's notes, which reads as "my note disappeared".
|
||||
el("scope").addEventListener("change", () => void refresh());
|
||||
el("write").addEventListener("click", async () => {
|
||||
await writeNote((el("scope") as HTMLSelectElement).value as Scope, val("title"), val("body"));
|
||||
await refresh();
|
||||
});
|
||||
el("openLink").addEventListener("click", async () => {
|
||||
const note = await readSharedNote(val("link"));
|
||||
el("openRef").addEventListener("click", async () => {
|
||||
el("shared").textContent = ""; // never show a previous answer beside a new question
|
||||
const note = await readSharedNote(val("reference"));
|
||||
el("shared").textContent = note ? `${note.title} — ${note.body}` : "(illisible)";
|
||||
});
|
||||
el("leave").addEventListener("click", async () => {
|
||||
el("left").textContent = "";
|
||||
await leaveMessage(val("onNote") as Nuri, val("message"));
|
||||
el("left").textContent = "déposé";
|
||||
});
|
||||
@@ -236,6 +257,8 @@ function wire(): void {
|
||||
const target = e.target as HTMLElement;
|
||||
const doc = target.dataset.doc as Nuri | undefined;
|
||||
if (!doc) return;
|
||||
el("shareResult").textContent = "";
|
||||
el("messages").textContent = "";
|
||||
if (target.classList.contains("share")) {
|
||||
await shareNote(doc, val("shareWith"));
|
||||
el("shareResult").textContent = "partagé";
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
button { cursor: pointer; border: 1px solid #bbb; border-radius: 5px; background: #f6f6f6; }
|
||||
ul { list-style: none; padding: 0; }
|
||||
li { padding: 6px 0; border-bottom: 1px solid #eee; }
|
||||
code.ref { font-size: 11px; color: #888; user-select: all; }
|
||||
.out { color: #555; font-size: 13px; min-height: 1.2em; }
|
||||
</style>
|
||||
</head>
|
||||
@@ -40,8 +41,8 @@
|
||||
|
||||
<fieldset>
|
||||
<legend>Ouvrir une note reçue</legend>
|
||||
<input id="link" data-testid="link" placeholder="lien de la note" size="46" />
|
||||
<button id="openLink" data-testid="open-link">ouvrir</button>
|
||||
<input id="reference" data-testid="reference" placeholder="référence de la note" size="46" />
|
||||
<button id="openRef" data-testid="open-reference">ouvrir</button>
|
||||
<p class="out" id="shared" data-testid="shared"></p>
|
||||
</fieldset>
|
||||
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
"paths": {
|
||||
"@ng-eventually/sdk": [
|
||||
"../../packages/sdk/src/index.ts"
|
||||
],
|
||||
"@ng-org/web": [
|
||||
"../../node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user