feat(example): une app d'exemple, écrite comme un consommateur
Le harnais e2e parlait à un sac de méthodes posé sur `window.__sdk`. Il prouvait que les fonctions s'exécutaient, jamais qu'on pouvait écrire une application avec — et cet écart a livré un vrai défaut : l'inbox d'un document était verte en test et inutilisable en vrai, parce que le harnais faisait traverser une adresse d'une identité à l'autre par une variable, ce qu'aucune application ne peut faire. `examples/notebook` est une application minimale en DOM natif, qui résout `@ng-eventually/client` comme un consommateur externe (workspace, dépendance déclarée, aucun import privilégié). Elle ne peut faire que ce qu'une application peut faire. Elle s'est déjà payée deux fois pendant son écriture : - `UnionSubject.subject` et `.graph` étaient typés `string` alors que ce sont toujours des références de document. Un consommateur devait donc caster ce qu'il venait de lire avant de le repasser — un cast à cet endroit précis rouvre la confusion que les types template literal existent pour fermer. - l'écran d'accès normalisait ce que l'utilisateur SAISIT mais pas ce que l'URL porte, si bien qu'un lien `?ng-id=@Erin` ouvrait un espace différent de celui de la même personne tapant `erin`. Une seule normalisation désormais, celle du registre. Le domaine est volontairement mince — des notes — mais suffit à exercer le placement par scope, la possession de caps, le partage dirigé, les inbox par document et la lecture réactive. 170 tests unitaires, typecheck vert sur la lib, l'exemple et le harnais.
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
configure,
|
||||
configureStoreRegistry,
|
||||
setCurrentUser,
|
||||
getCurrentUser,
|
||||
capFor,
|
||||
getCaps,
|
||||
resetCaps,
|
||||
@@ -40,7 +41,7 @@ import {
|
||||
// `storeRegistry` above is the app-facing slice; these are the shim internals.
|
||||
import * as registryInternals from "../src/shared-wallet/account-registry";
|
||||
import * as virtualUsers from "../src/shared-wallet/virtual-users";
|
||||
import { isNuri } from "@ng-eventually/client";
|
||||
import { isNuri, ensureIdentity } from "@ng-eventually/client";
|
||||
import type { Nuri, ShapeObservable, ShapeQuery } from "@ng-eventually/client";
|
||||
|
||||
const { IdentityStore } = virtualUsers;
|
||||
@@ -96,6 +97,10 @@ configure({
|
||||
ng: realNg,
|
||||
useShape: fakeUseShape,
|
||||
init: realInit,
|
||||
// The harness already holds an open wallet (the suite imports it itself), so it never
|
||||
// needs the gate's assisted import. These values exist only so the gate has something
|
||||
// to render when the access-gate steps exercise it — they are never used to import.
|
||||
sharedWallet: { fileUrl: "/harness-not-used.ngw", password: "harness" },
|
||||
});
|
||||
|
||||
configureStoreRegistry({
|
||||
@@ -167,6 +172,50 @@ const identity = new IdentityStore(
|
||||
return { walletName, b64: btoa(bin), len: bytes.length };
|
||||
},
|
||||
|
||||
/**
|
||||
* THE ACCESS GATE, in a real browser — ported from the consumer's
|
||||
* `barriere-acces-identifiant` feature, which the library took over with the flow.
|
||||
*
|
||||
* Unit tests pin the resolution ORDER (`test/access-gate.test.ts`); only a real DOM can
|
||||
* pin the barrier itself: that it appears on a first access, that entering a value
|
||||
* settles the identity normalized, and — the one that matters most — that it does NOT
|
||||
* appear when the identity is already known, since a returning user seeing the barrier
|
||||
* again is the visible face of the silent bug (a second virtual space).
|
||||
*/
|
||||
async accessGateFirstVisit(raw: string) {
|
||||
setCurrentUser(null);
|
||||
try { window.localStorage.removeItem("ng-eventually:identity"); } catch {}
|
||||
const done = ensureIdentity();
|
||||
const gate = document.querySelector('[data-ng-eventually="access-gate"]');
|
||||
const root = gate?.shadowRoot ?? null;
|
||||
const input = root?.querySelector("input") as HTMLInputElement | null;
|
||||
const button = root?.querySelector("button.go") as HTMLButtonElement | null;
|
||||
const shown = input !== null && button !== null;
|
||||
const disabledWhenEmpty = button?.disabled ?? null;
|
||||
if (input && button) {
|
||||
input.value = raw;
|
||||
input.dispatchEvent(new Event("input"));
|
||||
button.click();
|
||||
}
|
||||
await done;
|
||||
return {
|
||||
shown,
|
||||
disabledWhenEmpty,
|
||||
identity: getCurrentUser(),
|
||||
stillMounted: document.querySelector('[data-ng-eventually="access-gate"]') !== null,
|
||||
};
|
||||
},
|
||||
|
||||
/** The barrier must stay away once an identity is known. */
|
||||
async accessGateReturningVisit(known: string) {
|
||||
setCurrentUser(known);
|
||||
await ensureIdentity();
|
||||
return {
|
||||
shown: document.querySelector('[data-ng-eventually="access-gate"]') !== null,
|
||||
identity: getCurrentUser(),
|
||||
};
|
||||
},
|
||||
|
||||
// ── docs primitives ──────────────────────────────────────────────────────
|
||||
async docCreate() {
|
||||
const s = await sessionReady;
|
||||
|
||||
Reference in New Issue
Block a user