test(e2e): le parcours d'un primo-arrivant, et un harnais qui échoue au lieu de se pendre
Aucun parcours n'avait jamais marché le chemin d'un nouvel arrivant : tous pré-injectaient l'identifiant dans l'URL, ce qui fait résoudre l'identité sans jamais afficher la barrière. La suite était verte par-dessus un lien de téléchargement pointant sur un fichier que personne ne servait — et le serveur de test répondait la page HTML de l'application pour tout chemin inconnu, donc un fichier manquant ne POUVAIT pas échouer. Le nouveau parcours part d'un profil vide : barrière, téléchargement réel, import dans l'application portefeuille, saisie de l'identifiant, remise au broker, retour dans l'iframe. Il vérifie neuf points, dont celui qui compte — l'identifiant a survécu et l'identité rapportée est celle qui a été saisie. Le harnais, lui, se pendait au lieu d'échouer. Cause observée : le tuyau devtools de Chromium lâche et Playwright n'émet ni close ni disconnected, si bien que la suite bloquait dans son propre nettoyage sans imprimer ni résumé ni l'échec déjà en route. Toutes les attentes sont désormais bornées et nomment ce qu'elles attendaient ; vérifié en cassant délibérément une attente, et observé en conditions réelles — trois minutes et « gave up waiting for: alice to sign in » là où j'ai tué trois exécutions d'une heure ce matin. Deux exécutions simultanées ne se détruisent plus : verrou atomique sur le profil, et récupération d'un navigateur laissé par une exécution tuée. Le marqueur devient .user-consumed — il n'a jamais attesté d'une disponibilité, seulement qu'un lot avait déjà pris l'utilisateur de ce profil. Au passage, la destruction du profil dépendait du marqueur, écrit en FIN de lot : une exécution tuée avant laissait un profil que la suivante réutilisait, et héritait de sa casse. Elle dépend maintenant du profil. La suite applicative reste non mesurée sur cette machine : un conteneur en boucle de redémarrage recycle son interface réseau, et sept exécutions sur dix échouent sur le transport. Trois sont passées 21/21.
This commit is contained in:
+281
-39
@@ -16,15 +16,44 @@ import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { Socket } from "node:net";
|
||||
import {
|
||||
CONTEXT_ACTION_MS,
|
||||
CONTEXT_NAVIGATION_MS,
|
||||
browserLost,
|
||||
closeQuietly,
|
||||
within,
|
||||
} from "./deadline";
|
||||
import { isAlive } from "./run-lock";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// ── Dedicated, gitignored profile + wallet (NOT the app's .playwright-profile) ──
|
||||
export const PROFILE_DIR = path.resolve(__dirname, ".playwright-profile-lib");
|
||||
const WALLET_READY_MARKER = path.join(PROFILE_DIR, ".wallet-ready");
|
||||
/**
|
||||
* Marks that a batch has already taken the physical user held in this profile.
|
||||
*
|
||||
* Named for the only question it is ever asked. It is WRITTEN when a wallet is created and
|
||||
* READ in exactly one place — `ensureWallet`, to decide "discard this profile and mint a
|
||||
* fresh user". It never gated anything on readiness, and the previous name (`.wallet-ready`)
|
||||
* said it did: it cost a wrong diagnosis on 2026-08-11, where its presence was read as
|
||||
* "the wallet is good to use" when it means the opposite — this user belongs to a batch
|
||||
* that is over. `user` rather than `wallet` because what a batch consumes is an identity;
|
||||
* the wallet is only its container (see the note on ensureWallet below).
|
||||
*/
|
||||
const USER_CONSUMED_MARKER = path.join(PROFILE_DIR, ".user-consumed");
|
||||
export const WALLET_NAME = "ng-eventually-e2e";
|
||||
export const WALLET_PASSWORD = "ng-eventually-e2e";
|
||||
|
||||
/** `bun build` is a local bundle; a minute is already ten times what it takes. */
|
||||
const BUILD_MS = 60_000;
|
||||
/** Launching a browser is local too — 30s is Playwright's own default, doubled. */
|
||||
const LAUNCH_MS = 60_000;
|
||||
/** Opening a page in a live browser is instant; a minute means the browser is not answering. */
|
||||
const NEW_PAGE_MS = 60_000;
|
||||
/** The whole wallet export measures ~7s against the real broker; two minutes is a hang. */
|
||||
const EXPORT_MS = 120_000;
|
||||
|
||||
const ENTRY = path.resolve(__dirname, "polyfill-entry.ts");
|
||||
const BUNDLE_OUT = path.resolve(__dirname, ".dist", "polyfill-entry.js");
|
||||
|
||||
@@ -47,13 +76,48 @@ export function buildBundle(): void {
|
||||
execSync(`bun build ${ENTRY} --outfile ${BUNDLE_OUT} --bundle --format=esm`, {
|
||||
stdio: "pipe",
|
||||
cwd: path.resolve(__dirname, ".."),
|
||||
timeout: BUILD_MS,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve `page` and `routes` on an ephemeral port, and hand back a close that CLOSES.
|
||||
*
|
||||
* `server.close()` alone stops the listener and then waits for every keep-alive connection
|
||||
* to drain on its own — a browser that is still attached keeps the server half-alive long
|
||||
* after the harness believes it gone. These suites close a server while a browser is still
|
||||
* pointed at it (the wallet export does exactly that), so the sockets are tracked and
|
||||
* destroyed: "closed" has to mean closed, or the next thing to go wrong gets blamed on the
|
||||
* suite instead of on the connection nobody hung up.
|
||||
*/
|
||||
export function serveOnEphemeralPort(
|
||||
handler: (req: http.IncomingMessage, res: http.ServerResponse) => void,
|
||||
): Promise<{ url: string; close: () => void }> {
|
||||
const server = http.createServer(handler);
|
||||
const open = new Set<Socket>();
|
||||
server.on("connection", (socket) => {
|
||||
open.add(socket);
|
||||
socket.on("close", () => open.delete(socket));
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const port = (server.address() as { port: number }).port;
|
||||
resolve({
|
||||
url: `http://127.0.0.1:${port}`,
|
||||
close: () => {
|
||||
server.close();
|
||||
for (const socket of open) socket.destroy();
|
||||
open.clear();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function serveHarness(): Promise<{ url: string; close: () => void }> {
|
||||
const bundle = fs.readFileSync(BUNDLE_OUT, "utf-8");
|
||||
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>ng-eventually polyfill e2e</title></head><body><div id="root"></div><script type="module" src="/polyfill-entry.js"></script></body></html>`;
|
||||
const server = http.createServer((req, res) => {
|
||||
return serveOnEphemeralPort((req, res) => {
|
||||
if (req.url === "/polyfill-entry.js") {
|
||||
res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8" });
|
||||
res.end(bundle);
|
||||
@@ -62,12 +126,73 @@ export function serveHarness(): Promise<{ url: string; close: () => void }> {
|
||||
res.end(html);
|
||||
}
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const port = (server.address() as { port: number }).port;
|
||||
resolve({ url: `http://127.0.0.1:${port}`, close: () => server.close() });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── every browser this harness opens, launched and watched the same way ───────
|
||||
|
||||
/** Contexts we are closing ON PURPOSE — so their `close` event is not read as a loss. */
|
||||
const closingOnPurpose = new WeakSet<BrowserContext>();
|
||||
|
||||
/**
|
||||
* Launch a persistent context on `dir`, bounded, with the harness's own timeouts applied
|
||||
* and its disappearance turned into an immediate, named failure.
|
||||
*
|
||||
* The watch is the load-bearing part. VERIFIED 2026-08-11: this suite's browser can exit
|
||||
* mid-run — the devtools pipe between the runner and Chromium is terminated and Chromium
|
||||
* shuts down (exitCode=0) — and Playwright does NOT reject the calls already waiting on it.
|
||||
* A bounded wait then burns its whole timeout; an unbounded one (`newPage`, `evaluate`,
|
||||
* and `context.close()` in a `finally`) waits for ever. That is how a 60-second failure
|
||||
* became three runs killed at 50 and 68 minutes having printed nothing.
|
||||
*
|
||||
* So the context's own `close` event is listened to, and anything it was not asked to do
|
||||
* is declared a loss once, loudly, for every wait at once.
|
||||
*/
|
||||
async function launchWatchedContext(label: string, dir: string): Promise<BrowserContext> {
|
||||
const ctx = await within(`the ${label} browser to launch`, LAUNCH_MS, () =>
|
||||
chromium.launchPersistentContext(dir, {
|
||||
headless: true,
|
||||
executablePath: resolveChromePath(),
|
||||
args: LAUNCH_ARGS,
|
||||
timeout: LAUNCH_MS,
|
||||
}),
|
||||
);
|
||||
ctx.setDefaultTimeout(CONTEXT_ACTION_MS);
|
||||
ctx.setDefaultNavigationTimeout(CONTEXT_NAVIGATION_MS);
|
||||
const gone = (how: string) => {
|
||||
if (closingOnPurpose.has(ctx)) return;
|
||||
browserLost(
|
||||
`the ${label} browser went away mid-run — ${how}. Every wait on it is now ` +
|
||||
"unanswerable, so the run stops here instead of waiting on a browser that " +
|
||||
"no longer exists",
|
||||
);
|
||||
};
|
||||
// Both signals, and NEITHER of them covers the loss that hurts most — which is the whole
|
||||
// reason the deadlines above are not optional.
|
||||
//
|
||||
// VERIFIED 2026-08-11: on a normal teardown both `close` and `disconnected` fire. On the
|
||||
// failure this harness actually suffers — Chromium logging "Connection terminated while
|
||||
// reading from pipe" and exiting — Playwright fires NEITHER, four times out of four. Its
|
||||
// client never learns the pipe is gone, so every call already in flight simply waits, and
|
||||
// every call after it waits too. That is why a browser dying used to cost an hour of
|
||||
// silence, and why no event-based guard can be the protection here: only a deadline can.
|
||||
//
|
||||
// They are wired anyway because they DO catch the losses they can see (a context closed by
|
||||
// something nobody asked), and those are free to catch immediately rather than at the end
|
||||
// of a bound.
|
||||
ctx.on("close", () => gone("its context closed and nobody asked it to"));
|
||||
ctx.browser()?.on("disconnected", () => gone("its devtools connection dropped"));
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/** Close a context we own, bounded, without its `close` event being read as a loss. */
|
||||
export async function closeContext(label: string, ctx: BrowserContext): Promise<void> {
|
||||
closingOnPurpose.add(ctx);
|
||||
await closeQuietly(`the ${label} context`, () => ctx.close());
|
||||
}
|
||||
|
||||
/** Open a page under a bound: `context.newPage()` carries no timeout of its own. */
|
||||
export function newPage(label: string, ctx: BrowserContext): Promise<Page> {
|
||||
return within(`a new page for ${label}`, NEW_PAGE_MS, () => ctx.newPage());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,23 +219,80 @@ export function serveHarness(): Promise<{ url: string; close: () => void }> {
|
||||
* CONTRACT 1 and 2 test exactly that (a faithful reconnect over the same profile, and
|
||||
* the absence of an account fork across it).
|
||||
*/
|
||||
/**
|
||||
* Make sure no browser is still holding `dir`, and kill the one that is.
|
||||
*
|
||||
* ── Why a run has to do this ─────────────────────────────────────────────────
|
||||
* A run that fails or is killed leaves its Chromium ALIVE — `BrowserContext.close()` in the
|
||||
* teardown gives up after its bound (and a `kill -9` on the runner never gets there at all).
|
||||
* That orphan keeps the shared profile open, and the NEXT run then deletes the directory
|
||||
* under it and launches a second Chromium on the same path. Chromium's process singleton
|
||||
* settles that argument by having the newcomer hand over and quit — which the runner sees
|
||||
* as its devtools pipe dying moments after launch (VERIFIED 2026-08-11: "Connection
|
||||
* terminated while reading from pipe" 170 ms after `<launched>`).
|
||||
*
|
||||
* So one failure poisons every run after it, each faster than the last, and none of them is
|
||||
* about the thing under test. Reclaiming the profile is what stops the cascade.
|
||||
*
|
||||
* Chromium names the holder itself: `SingletonLock` is a symlink to `<host>-<pid>`. And the
|
||||
* run lock has already established that no LEGITIMATE run is alive — so whatever holds this
|
||||
* profile is debris, and killing it is safe.
|
||||
*/
|
||||
async function reclaimProfile(dir: string): Promise<void> {
|
||||
let target: string;
|
||||
try {
|
||||
target = fs.readlinkSync(path.join(dir, "SingletonLock"));
|
||||
} catch {
|
||||
return; // no lock, nothing holding it
|
||||
}
|
||||
const pid = Number(target.slice(target.lastIndexOf("-") + 1));
|
||||
if (!Number.isInteger(pid) || pid <= 0 || !isAlive(pid)) return;
|
||||
|
||||
console.warn(
|
||||
`[e2e] a browser from an earlier run (pid ${pid}) still holds ${path.basename(dir)} — ` +
|
||||
"killing it, or this run's own browser would be refused the profile and quit",
|
||||
);
|
||||
try {
|
||||
process.kill(pid, "SIGKILL");
|
||||
} catch {
|
||||
return; // gone between the check and the signal
|
||||
}
|
||||
const deadline = Date.now() + 10_000;
|
||||
while (isAlive(pid) && Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
if (isAlive(pid)) {
|
||||
throw new Error(
|
||||
`[e2e] could not reclaim ${dir}: pid ${pid} survived SIGKILL. Nothing this run does ` +
|
||||
"next would be measuring the code under test — stop and clear it by hand.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureWallet(): Promise<void> {
|
||||
if (fs.existsSync(WALLET_READY_MARKER)) {
|
||||
const age = Date.now() - fs.statSync(WALLET_READY_MARKER).mtimeMs;
|
||||
await reclaimProfile(PROFILE_DIR);
|
||||
// Discard on the PROFILE's existence, not on the marker's.
|
||||
//
|
||||
// Keyed on the marker, a profile left behind by a batch that died BEFORE writing it was
|
||||
// silently REUSED — precisely the opposite of the per-batch rule below, and a profile
|
||||
// half-way through wallet creation makes the nextgraph.eu flow fail on a screen the code
|
||||
// does not expect ("element was detached from the DOM"). Worse, it is self-perpetuating:
|
||||
// the failed run writes no marker either, so every later run inherits the same wreck.
|
||||
// Observed 2026-08-11. The marker now only says HOW OLD the discarded user was.
|
||||
if (fs.existsSync(PROFILE_DIR)) {
|
||||
const age = fs.existsSync(USER_CONSUMED_MARKER)
|
||||
? `${Math.round((Date.now() - fs.statSync(USER_CONSUMED_MARKER).mtimeMs) / 60000)} min old`
|
||||
: "left by a batch that did not finish";
|
||||
console.log(
|
||||
`[e2e] discarding the previous batch's wallet (${Math.round(age / 60000)} min old) — ` +
|
||||
`[e2e] discarding the previous batch's wallet (${age}) — ` +
|
||||
"a physical user is per-batch, see ensureWallet",
|
||||
);
|
||||
fs.rmSync(PROFILE_DIR, { recursive: true, force: true });
|
||||
}
|
||||
console.log("[e2e] creating this batch's wallet on nextgraph.eu...");
|
||||
fs.mkdirSync(PROFILE_DIR, { recursive: true });
|
||||
const ctx = await chromium.launchPersistentContext(PROFILE_DIR, {
|
||||
headless: true,
|
||||
executablePath: resolveChromePath(),
|
||||
args: LAUNCH_ARGS,
|
||||
});
|
||||
const page = ctx.pages()[0] || (await ctx.newPage());
|
||||
const ctx = await launchWatchedContext("wallet-creation", PROFILE_DIR);
|
||||
const page = ctx.pages()[0] || (await newPage("the wallet creation flow", ctx));
|
||||
page.on("pageerror", () => {});
|
||||
try {
|
||||
await page.goto("https://nextgraph.eu/", { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||
@@ -150,22 +332,18 @@ export async function ensureWallet(): Promise<void> {
|
||||
await page.waitForTimeout(10000);
|
||||
console.log("[e2e] dedicated lib wallet created + bootstrapped");
|
||||
} finally {
|
||||
await ctx.close();
|
||||
await closeContext("wallet-creation", ctx);
|
||||
}
|
||||
fs.writeFileSync(WALLET_READY_MARKER, new Date().toISOString());
|
||||
fs.writeFileSync(USER_CONSUMED_MARKER, new Date().toISOString());
|
||||
}
|
||||
|
||||
export async function launchWalletContext(): Promise<BrowserContext> {
|
||||
return chromium.launchPersistentContext(PROFILE_DIR, {
|
||||
headless: true,
|
||||
executablePath: resolveChromePath(),
|
||||
args: LAUNCH_ARGS,
|
||||
});
|
||||
export async function launchWalletContext(label = "wallet"): Promise<BrowserContext> {
|
||||
return launchWatchedContext(label, PROFILE_DIR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a BRAND-NEW wallet in a BRAND-NEW profile dir and RETURN the launched
|
||||
* context, without a `.wallet-ready` marker and WITHOUT tearing the context down.
|
||||
* context, without a `.user-consumed` marker and WITHOUT tearing the context down.
|
||||
* Unlike {@link ensureWallet} (which reuses one persistent dedicated wallet across
|
||||
* runs — so it is always "hot"), this mints a genuinely FRESH wallet each call so
|
||||
* the cold-start (private-store repo not yet in `self.repos`) can be exercised.
|
||||
@@ -182,12 +360,8 @@ export async function createFreshWalletContext(): Promise<{
|
||||
}> {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ng-eventually-fresh-"));
|
||||
const name = "ng-fresh-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
||||
const ctx = await chromium.launchPersistentContext(dir, {
|
||||
headless: true,
|
||||
executablePath: resolveChromePath(),
|
||||
args: LAUNCH_ARGS,
|
||||
});
|
||||
const page = ctx.pages()[0] || (await ctx.newPage());
|
||||
const ctx = await launchWatchedContext("fresh-wallet", dir);
|
||||
const page = ctx.pages()[0] || (await newPage("the fresh wallet creation flow", ctx));
|
||||
page.on("pageerror", () => {});
|
||||
await page.goto("https://nextgraph.eu/", { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||
const createButton = page.getByText("Create Wallet", { exact: true });
|
||||
@@ -239,22 +413,75 @@ export async function createFreshWalletContext(): Promise<{
|
||||
*/
|
||||
export async function launchCleanProfileContext(): Promise<{ ctx: BrowserContext; dir: string }> {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ng-eventually-clean-"));
|
||||
const ctx = await chromium.launchPersistentContext(dir, {
|
||||
headless: true,
|
||||
executablePath: resolveChromePath(),
|
||||
args: LAUNCH_ARGS,
|
||||
});
|
||||
const ctx = await launchWatchedContext("clean-profile", dir);
|
||||
return { ctx, dir };
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize THIS batch's wallet as a `.ngw` file at `ngwPath`, and return its size.
|
||||
*
|
||||
* A wallet's bytes exist only inside the broker iframe — `ng.wallet_get_file()` is an RPC
|
||||
* to the wallet the broker holds — so nothing in Node can produce one. The harness page
|
||||
* already exposes the call (`polyfill-entry.ts`, `exportWalletFile`), and `run.ts` uses it
|
||||
* for the clean-profile cold-start; this wraps the same round-trip for callers that hold
|
||||
* no harness frame of their own.
|
||||
*
|
||||
* Why the APPLICATIVE suite needs it: the access gate HANDS A WALLET FILE OUT. Serving a
|
||||
* placeholder there would make the download step a decoration — the file has to be a real
|
||||
* wallet, or importing it cannot let anybody in.
|
||||
*/
|
||||
export async function exportWalletNgw(ctx: BrowserContext, ngwPath: string): Promise<number> {
|
||||
buildBundle();
|
||||
const { url, close } = await serveHarness();
|
||||
const page = await newPage("the wallet export", ctx);
|
||||
page.on("pageerror", () => {});
|
||||
try {
|
||||
const frame = await setupBrokerPage(page, url);
|
||||
await frame.waitForFunction(
|
||||
() => (window as unknown as { __sdk?: unknown }).__sdk !== undefined,
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
await frame.waitForFunction(
|
||||
() => (window as unknown as { __sdk: { status(): string } }).__sdk.status() === "connected",
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
// `frame.evaluate` has NO timeout of its own — a bridge call that never settles is one
|
||||
// of the two ways this harness used to hang for ever. The whole export measures ~7s.
|
||||
const exported = await within("the wallet bytes from the broker iframe", EXPORT_MS, () =>
|
||||
frame.evaluate(() =>
|
||||
(
|
||||
window as unknown as {
|
||||
__sdk: { exportWalletFile(): Promise<{ walletName: string; b64: string; len: number }> };
|
||||
}
|
||||
).__sdk.exportWalletFile(),
|
||||
),
|
||||
);
|
||||
fs.writeFileSync(ngwPath, Buffer.from(exported.b64, "base64"));
|
||||
return exported.len;
|
||||
} finally {
|
||||
await closeQuietly("the wallet export page", () => page.close());
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a `.ngw` wallet FILE into the current (clean) profile via the standalone
|
||||
* nextgraph.eu "Import a Wallet File" flow, then unlock it with the password. After
|
||||
* this the profile holds the wallet — but NOT the repos' local cache — so the next
|
||||
* SDK session over it hits the broker-only cold-start. Adapted from the Festipod
|
||||
* app's `importWalletViaFile` (the proven real-broker wallet-file import).
|
||||
*
|
||||
* The password is a PARAMETER, defaulting to the batch wallet's. The applicative suite
|
||||
* reads it off the access gate's own screen and passes it here — which is the only way a
|
||||
* test can tell that what the barrier DISPLAYS is what actually opens the file. Hard-coding
|
||||
* it here would make that step untestable: the import would succeed on a barrier showing
|
||||
* anything at all, including nothing.
|
||||
*/
|
||||
export async function importWalletViaFile(page: Page, ngwPath: string): Promise<void> {
|
||||
export async function importWalletViaFile(
|
||||
page: Page,
|
||||
ngwPath: string,
|
||||
password: string = WALLET_PASSWORD,
|
||||
): Promise<void> {
|
||||
await page.goto("https://nextgraph.eu/#/wallet/login", { waitUntil: "domcontentloaded" });
|
||||
// Let the SPA render + attach the file input (uploading too early → EncryptionError).
|
||||
await page.waitForTimeout(3000);
|
||||
@@ -262,7 +489,7 @@ export async function importWalletViaFile(page: Page, ngwPath: string): Promise<
|
||||
await page.setInputFiles('input[type=file]', ngwPath);
|
||||
const passwordInput = page.locator('input[type=password]').first();
|
||||
await passwordInput.waitFor({ state: "visible", timeout: 15000 });
|
||||
await passwordInput.fill(WALLET_PASSWORD);
|
||||
await passwordInput.fill(password);
|
||||
await passwordInput.press("Enter");
|
||||
const confirm = page.getByRole("button", { name: /Confirm/i });
|
||||
if (await confirm.isVisible({ timeout: 2000 }).catch(() => false)) await confirm.click().catch(() => {});
|
||||
@@ -277,7 +504,22 @@ export async function importWalletViaFile(page: Page, ngwPath: string): Promise<
|
||||
export async function setupBrokerPage(page: Page, appUrl: string): Promise<Frame> {
|
||||
const brokerRedirect = `https://nextgraph.net/redir/#/?o=${encodeURIComponent(appUrl)}`;
|
||||
await page.goto(brokerRedirect, { waitUntil: "domcontentloaded" });
|
||||
return completeBrokerLogin(page, appUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* The half of {@link setupBrokerPage} that does NOT navigate: unlock if a login is shown,
|
||||
* then wait for the application's iframe and return it.
|
||||
*
|
||||
* Split out because there are two ways to arrive at the broker, and only one of them is
|
||||
* the suite's. A test signs the round-trip off itself (`setupBrokerPage`); an APPLICATION
|
||||
* hands the page over on its own, inside `init()`, once the identity is settled — and a
|
||||
* journey that walks a first-time user through the barrier has to let it, because that
|
||||
* hand-over IS what it is checking. Calling `setupBrokerPage` there would re-navigate and
|
||||
* throw away the URL the application had just built, `?ng-id=` included — i.e. it would
|
||||
* quietly substitute the suite's path for the one under test.
|
||||
*/
|
||||
export async function completeBrokerLogin(page: Page, appUrl: string): Promise<Frame> {
|
||||
const loginButton = page.getByText("Login", { exact: true });
|
||||
if (await loginButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await loginButton.click();
|
||||
|
||||
Reference in New Issue
Block a user