Files
ng-eventually/packages/ng-e2e-helpers/src/nextgraph-ui.ts
T
Sylvain Duchesne 1271d48e9f refactor(e2e): la mécanique de test devient un paquet à part, ng-e2e-helpers
Créer un portefeuille, en obtenir le .ngw, traverser le broker : ce n'est pas du
ressort du polyfill. C'est un besoin commun au polyfill et à toute application
NextGraph — et surtout, ça SURVIT à la migration, alors que le polyfill est fait
pour disparaître. L'y laisser, c'était le faire mourir avec lui ou rendre le
polyfill indéracinable.

Le paquet n'importe rien du polyfill — vérifié mécaniquement — et déclare
Playwright et @ng-org/web en pairs, le consommateur devant maîtriser les
versions. Sa surface : attentes bornées, mesure, navigateur, profils,
portefeuille, traversée du broker, rapport d'exécution, et reconnaissance des
modes de panne connus.

La preuve qu'il est utilisable de l'extérieur : le polyfill le CONSOMME, sans
garder de copie. Restent chez lui les parcours, la barrière et les identités
virtuelles, qui lui sont propres.

Le verrou entre exécutions disparaît, remplacé par un profil par exécution. Il
ne traitait qu'un symptôme — un répertoire partagé que la création de
portefeuille effaçait. Avec un profil par exécution il n'y a plus rien à
sérialiser, les exécutions concurrentes deviennent indépendantes, et la
collision entre deux dépôts s'évanouit au lieu d'être exportée. Six exécutions :
aucun répertoire ni Chromium orphelin.

Et la connaissance descriptive est séparée du pilotage : URL, sélecteurs et
inventaire ordonné des écrans sont des données, passées DANS la page pour la
reconnaissance — donc un échec nomme le même écran que celui sur lequel on
dispatchait.

Un échec de navigateur est désormais nommé comme tel — « the actors browser
STOPPED ANSWERING » — au lieu de sortir sous le nom de l'opération innocente qui
se trouvait en vol.
2026-08-16 14:16:11 +02:00

187 lines
8.8 KiB
TypeScript

/**
* What NextGraph's own pages LOOK like — addresses, selectors, and the inventory of screens
* the sign-in walks through. Description only: nothing here drives a browser.
*
* ── Why it is a separate file, and why it is data ────────────────────────────
* Two kinds of knowledge live in this package and they age at completely different rates.
* How to cross a broker — dispatch on the screen you can see, never on elapsed time; identify
* the application by its origin, never by a substring — is a *method*, and it has survived
* every change upstream. WHICH selector shows a wallet list is a *fact about today's markup*,
* and it changes whenever the wallet application is restyled.
*
* Keeping the second kind as plain data has two consequences worth the split. Upstream
* changes a selector: you edit a string in this file and no control flow moves. And the
* driving code below (`broker.ts`, `wallet.ts`) reads this inventory rather than embedding
* it, so a harness built on some other browser driver would reuse this file whole and
* rewrite only the driving. That adapter is NOT built here — the point is only that
* building it would not be a rewrite.
*
* The screen inventory is deliberately SERIALIZABLE: it is handed to the browser as an
* argument (see `readBrokerScreen` in `broker.ts`), so the same description that names a
* screen in a failure message is the one the recognition dispatched on. That rules out
* regular expressions as values, hence {@link TextPattern}.
*
* VERIFIED 2026-08-14 against the live pages unless noted; the upstream source is
* `nextgraph-rs` (`infra/ngnet/redir`, `engine/broker/auth`, `app/ui-common`), read but
* never modified.
*/
// ── the wallet application (nextgraph.eu) ───────────────────────────────────
/**
* Where a wallet is created and where one is imported. The wallet application is a real
* application like any other — this harness drives its actual interface rather than
* reaching behind it, because a wallet obtained any other way is not the one a person has.
*/
export const WALLET_APP = {
home: "https://nextgraph.eu/",
/** The standalone import/unlock route, reachable without going through the broker. */
login: "https://nextgraph.eu/#/wallet/login",
} as const;
/** The creation flow, screen by screen, as labels and selectors. */
export const WALLET_CREATION = {
/** Step 1 — the home page's entry point. */
createWallet: "Create Wallet",
/** Step 2 — the terms screen, reached on the `/account` route. */
acceptTerms: "I accept",
/** The URL glob that route is awaited by. */
termsRoute: "**/account*",
/** Step 3 — the credentials form. */
username: "#username-input",
password: "#password-input",
/** Matched loosely: the button's caption is not stable in case. */
submit: "create my wallet",
/** Step 4 — creation lands here, and the first unlock happens from it. */
landsOn: "**/#/wallet/login",
/** Offered on the login route when a wallet is already on the device. */
loginWithThisWallet: "Click here to login with your wallet",
passwordField: 'input[type="password"]',
} as const;
/** The import-a-wallet-file flow on the same login route. */
export const WALLET_IMPORT = {
fileInput: "input[type=file]",
passwordField: "input[type=password]",
/** Shown by some builds after the password; absent in others, so it is probed, not awaited. */
confirm: /Confirm/i,
} as const;
// ── the broker crossing (nextgraph.net/redir → the broker's auth page) ──────
/** The redirect that hands an application's address to the broker. */
export function brokerRedirectFor(appUrl: string): string {
return `https://nextgraph.net/redir/#/?o=${encodeURIComponent(appUrl)}`;
}
/**
* The distinct screens the crossing can be on.
*
* - `choose-broker` — the redirect page with MORE than one broker to pick from. Not observed
* on hosts that resolve to a single broker (which auto-selects), so it is described from
* the upstream source rather than from observation.
* - `login-offered` — "We could not find a wallet on this device… Login". The entry screen of
* every sign-in observed, first actor and later ones alike.
* - `wallet-list` — "Select a wallet to login with", one box per wallet.
* - `password` — "Enter your password". Reached by the FIRST actor only: the wallet is
* broadcast between the broker origin's tabs over a `BroadcastChannel` named `ng_wallet`,
* so a later actor's wallet is already in `opened_wallets` and selecting it logs straight
* in (`ui-common/src/routes/WalletLogin.svelte`, the `$opened_wallets[selected]` path).
* VERIFIED 2026-08-14, three consecutive sign-ins in one browser context.
* - `working` — a splash, "Opening your wallet…", "Wallet opened for …". Nothing to do but
* wait for it to become something else. Note that SUCCESS is one of these: the final screen
* never stops being `working`, which is why the application's frame is watched separately
* rather than inferred from the screen.
* - `error` — the broker said no ("An error occurred", "Invalid request"). Terminal.
*/
export type BrokerScreen =
| "choose-broker"
| "login-offered"
| "wallet-list"
| "password"
| "working"
| "error";
/**
* A regular expression as data, because the inventory crosses into the browser and a
* `RegExp` does not survive that trip. Rebuilt on the far side with `new RegExp(...)`.
*/
export interface TextPattern {
readonly source: string;
readonly flags: string;
}
/** How a screen is told apart from the ones described BEFORE it. */
export type ScreenSignature =
/** Any of these selectors matches an element with a non-zero box. */
| { readonly kind: "rendered"; readonly selectors: readonly string[] }
/** A rendered `<button>`/`<a>` whose trimmed text matches. */
| { readonly kind: "rendered-control"; readonly matches: TextPattern }
/** The page's RENDERED prose matches — the one test that has to read words. */
| { readonly kind: "page-text"; readonly matches: TextPattern }
/** Whatever is left. Must be the last entry, and there must be one. */
| { readonly kind: "otherwise" };
/** What moves the flow on from a screen. */
export type ScreenResponse =
| { readonly kind: "click"; readonly what: string; readonly selector: string }
| { readonly kind: "click-text"; readonly what: string; readonly text: string }
/** Fill the run's wallet password and submit it. The password is never described here —
* it belongs to the run, not to the pages. */
| { readonly kind: "submit-password"; readonly what: string; readonly selector: string }
/** Nothing to do but let it become something else. */
| { readonly kind: "wait" };
export interface BrokerScreenSpec {
readonly screen: BrokerScreen;
readonly signature: ScreenSignature;
readonly answer: ScreenResponse;
/** Terminal: reaching it ends the crossing with a failure rather than an action. */
readonly terminal?: true;
}
/**
* The inventory, IN THE ORDER IT IS TESTED — and the order is load-bearing, not cosmetic.
*
* Each screen is identified by the signature that the screens BEFORE it do not have.
* Visibility is checked by measured box rather than by presence, because the auth
* application HIDES its whole login UI (`#app` gets `display:none`) instead of removing it
* once the wallet is open — a presence test would keep reporting `wallet-list` on a page
* that has already logged in.
*/
export const BROKER_SCREENS: readonly BrokerScreenSpec[] = [
{
screen: "password",
signature: { kind: "rendered", selectors: ["#password-input", 'input[type="password"]'] },
answer: { kind: "submit-password", what: "the password", selector: "#password-input, input[type='password']" },
},
{
screen: "wallet-list",
signature: { kind: "rendered", selectors: [".wallet-box"] },
// The BOX, not its caption: the caption only renders for a wallet that carries a
// password, and the box is the thing with `role="button"` either way.
answer: { kind: "click", what: "this run's wallet", selector: ".wallet-box" },
},
{
screen: "choose-broker",
signature: { kind: "rendered", selectors: ['[role="menuitem"]'] },
answer: { kind: "click", what: "the first broker in the list", selector: '[role="menuitem"]' },
},
{
screen: "login-offered",
signature: { kind: "rendered-control", matches: { source: "^(login|anmelden)$", flags: "i" } },
answer: { kind: "click-text", what: 'the "Login" button', text: "Login" },
},
{
screen: "error",
signature: { kind: "page-text", matches: { source: "An error occurred|Invalid request", flags: "i" } },
answer: { kind: "wait" },
terminal: true,
},
{
screen: "working",
signature: { kind: "otherwise" },
answer: { kind: "wait" },
},
];