@nodefony/devkit 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +544 -0
- package/README.md +318 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +47 -0
- package/dist/nodefony/command/CardCommand.js +70 -0
- package/dist/nodefony/config/config.js +200 -0
- package/dist/nodefony/config/defineModuleConfig.js +36 -0
- package/dist/nodefony/controllers/DevkitController.js +60 -0
- package/dist/nodefony/controllers/McpController.js +223 -0
- package/dist/nodefony/controllers/OAuthMetadataController.js +89 -0
- package/dist/nodefony/interfaces/IDevkitService.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/DevkitService.js +198 -0
- package/dist/nodefony/src/card.js +2 -0
- package/dist/nodefony/src/errors/DevkitError.js +21 -0
- package/dist/nodefony/src/mcp/guard.js +51 -0
- package/dist/nodefony/src/mcp/protocol.js +127 -0
- package/dist/nodefony/src/mcp/server.js +133 -0
- package/dist/nodefony/src/mcp/tools.js +163 -0
- package/dist/types/index.d.ts +52 -0
- package/dist/types/nodefony/command/CardCommand.d.ts +33 -0
- package/dist/types/nodefony/config/config.d.ts +25 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/controllers/DevkitController.d.ts +37 -0
- package/dist/types/nodefony/controllers/McpController.d.ts +70 -0
- package/dist/types/nodefony/controllers/OAuthMetadataController.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +69 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +143 -0
- package/dist/types/nodefony/src/card.d.ts +18 -0
- package/dist/types/nodefony/src/errors/DevkitError.d.ts +14 -0
- package/dist/types/nodefony/src/mcp/guard.d.ts +66 -0
- package/dist/types/nodefony/src/mcp/protocol.d.ts +139 -0
- package/dist/types/nodefony/src/mcp/server.d.ts +48 -0
- package/dist/types/nodefony/src/mcp/tools.d.ts +81 -0
- package/docs/index.md +358 -0
- package/package.json +77 -0
- package/skills/nodefony-add-crud/SKILL.md +199 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +95 -0
- package/skills/nodefony-add-service/SKILL.md +90 -0
- package/skills/nodefony-browser/SKILL.md +416 -0
- package/skills/nodefony-browser/references/socket.md +115 -0
- package/skills/nodefony-browser/references/sondes.md +175 -0
- package/skills/nodefony-browser/scripts/audit.mjs +169 -0
- package/skills/nodefony-browser/scripts/inspect.mjs +903 -0
- package/skills/nodefony-browser/scripts/lib/browser.mjs +357 -0
- package/skills/nodefony-browser/scripts/lib/probes.mjs +501 -0
- package/skills/nodefony-browser/scripts/lib/wcag.mjs +153 -0
- package/skills/nodefony-browser/scripts/socket.mjs +354 -0
- package/skills/nodefony-browser/scripts/watch.mjs +125 -0
- package/skills/nodefony-migrate-schema/SKILL.md +359 -0
- package/skills/nodefony-migrate-schema/references/verdicts.md +139 -0
- package/skills/nodefony-protect-route/SKILL.md +195 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le décor commun aux sondes : lancer le navigateur, se connecter, ouvrir une
|
|
3
|
+
* page en étant sûr de mesurer CELLE-LÀ.
|
|
4
|
+
*
|
|
5
|
+
* Pourquoi une brique partagée plutôt que deux copies : les deux scripts sont
|
|
6
|
+
* copiés ensemble (`docker cp <dossier>/. <conteneur>:/app/`), donc la partager
|
|
7
|
+
* ne coûte rien — et la duplication précédente avait déjà divergé en silence,
|
|
8
|
+
* une seule des deux copies rattrapant un état d'authentification périmé.
|
|
9
|
+
*
|
|
10
|
+
* `@env` NF_BROWSER_BASE origine à joindre (défaut CONSTATÉ : 127.0.0.1 en local, host.docker.internal en conteneur)
|
|
11
|
+
* `@env` NF_BROWSER_OUT dossier des captures et de l'état d'authentification (défaut constaté de la même façon)
|
|
12
|
+
* `@env` NF_BROWSER_LOGIN chemin du formulaire de connexion — REQUIS dès qu'un identifiant est donné, aucun défaut n'est deviné
|
|
13
|
+
* `@env` NF_BROWSER_USER identifiant ; sans lui, aucune authentification n'est tentée
|
|
14
|
+
* `@env` NF_BROWSER_PASSWORD mot de passe associé
|
|
15
|
+
* `@env` NF_BROWSER_ENGINE navigateur imposé (chromium, chrome, msedge) ; sans lui, le premier qui répond
|
|
16
|
+
* `@env` NF_BROWSER_COLOR_SCHEME schéma de couleurs émulé (light, dark, no-preference) ; sans lui, celui du navigateur
|
|
17
|
+
* `@env` NF_BROWSER_STORAGE entrées de stockage local posées AVANT chargement (`clé=valeur`, séparées par des virgules)
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import {
|
|
22
|
+
environmentDefaults,
|
|
23
|
+
authStateName,
|
|
24
|
+
browserOrder,
|
|
25
|
+
parseColorScheme,
|
|
26
|
+
parseStorage,
|
|
27
|
+
} from "./probes.mjs";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Playwright — chargé À LA DEMANDE, pour pouvoir expliquer son absence.
|
|
31
|
+
*
|
|
32
|
+
* Il porte un navigateur de plus de cent mégaoctets : l'imposer à toute
|
|
33
|
+
* application qui installe cet outillage serait disproportionné, alors que
|
|
34
|
+
* seuls ceux qui veulent REGARDER un écran en ont besoin. Il est donc déclaré
|
|
35
|
+
* en pair optionnel — et un `MODULE_NOT_FOUND` nu, qui ne dit ni quoi
|
|
36
|
+
* installer ni pourquoi, n'est pas une réponse acceptable.
|
|
37
|
+
*/
|
|
38
|
+
let chromium;
|
|
39
|
+
try {
|
|
40
|
+
({ chromium } = await import("playwright"));
|
|
41
|
+
} catch {
|
|
42
|
+
console.error(
|
|
43
|
+
"Playwright est absent — c'est lui qui pilote le navigateur.\n\n" +
|
|
44
|
+
" npm i -D playwright && npx playwright install chromium\n\n" +
|
|
45
|
+
"Autre voie, si tu préfères ne rien poser sur ta machine : exécuter ces\n" +
|
|
46
|
+
"sondes dans un conteneur qui embarque déjà navigateur et pilote.",
|
|
47
|
+
);
|
|
48
|
+
process.exit(69); // EX_UNAVAILABLE
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Le décor : où joindre l'application, et où déposer ce qu'on produit.
|
|
53
|
+
*
|
|
54
|
+
* Les deux se CONSTATENT plutôt que de se supposer — `/.dockerenv` existe
|
|
55
|
+
* quand, et seulement quand, on s'exécute dans un conteneur. Le déduire de la
|
|
56
|
+
* plateforme serait faux dans les deux sens : un conteneur Linux sur un poste
|
|
57
|
+
* macOS, ou un poste Linux nu, rendraient le même `process.platform`.
|
|
58
|
+
*
|
|
59
|
+
* L'enjeu n'est pas cosmétique : `127.0.0.1` désigne le conteneur LUI-MÊME
|
|
60
|
+
* quand on y est enfermé, et la sonde mesurerait alors une connexion refusée
|
|
61
|
+
* en croyant que l'application est en panne.
|
|
62
|
+
*/
|
|
63
|
+
const IN_CONTAINER = existsSync("/.dockerenv");
|
|
64
|
+
const { base: baseUrl, out: OUT } = environmentDefaults({
|
|
65
|
+
inContainer: IN_CONTAINER,
|
|
66
|
+
base: process.env.NF_BROWSER_BASE,
|
|
67
|
+
out: process.env.NF_BROWSER_OUT,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** Où atterrissent captures et état d'authentification. */
|
|
71
|
+
export const OUTPUT = OUT;
|
|
72
|
+
export const BASE = baseUrl;
|
|
73
|
+
|
|
74
|
+
/** Les navigateurs à essayer, dans l'ordre — voir `browserOrder`. */
|
|
75
|
+
const BROWSERS = browserOrder(process.env.NF_BROWSER_ENGINE);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Le navigateur RÉELLEMENT utilisé, renseigné à l'ouverture.
|
|
79
|
+
*
|
|
80
|
+
* Rendu avec la mesure, parce que le décor en fait partie : un Chrome de
|
|
81
|
+
* système et le Chromium du pilote ne sont pas la même version, et deux
|
|
82
|
+
* chiffres de rendu comparés sans savoir cela ne comparent rien.
|
|
83
|
+
*/
|
|
84
|
+
export let browserUsed = null;
|
|
85
|
+
export const USER = process.env.NF_BROWSER_USER ?? "";
|
|
86
|
+
export const PASSWORD = process.env.NF_BROWSER_PASSWORD ?? "";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Le chemin du formulaire de connexion — celui de TON application.
|
|
90
|
+
*
|
|
91
|
+
* Aucun défaut : il n'existe pas d'écran de connexion universel, et deviner en
|
|
92
|
+
* enverrait la sonde sur une page inexistante, où elle « se connecterait »
|
|
93
|
+
* silencieusement avant de mesurer un écran d'erreur. Une ignorance ne doit
|
|
94
|
+
* jamais passer pour un contrôle réussi : on le dit, et on s'arrête.
|
|
95
|
+
*/
|
|
96
|
+
export const LOGIN = process.env.NF_BROWSER_LOGIN ?? "";
|
|
97
|
+
if (USER && !LOGIN) {
|
|
98
|
+
console.error(
|
|
99
|
+
"NF_BROWSER_USER est posé mais pas NF_BROWSER_LOGIN : donne le chemin de ton\n" +
|
|
100
|
+
"formulaire de connexion (par exemple /login), sinon la sonde n'a aucun moyen\n" +
|
|
101
|
+
"de savoir où s'authentifier.",
|
|
102
|
+
);
|
|
103
|
+
process.exit(64); // EX_USAGE
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* L'état d'authentification, SAUVEGARDÉ puis réutilisé d'une sonde à l'autre.
|
|
108
|
+
*
|
|
109
|
+
* Il vit dans le dossier de sortie — en conteneur, un volume monté, donc il
|
|
110
|
+
* survit à l'arrêt de celui-ci.
|
|
111
|
+
* Sans lui, chaque inspection rejoue le parcours de connexion — quelques
|
|
112
|
+
* secondes perdues et une occasion d'échec de plus à chaque exécution.
|
|
113
|
+
*
|
|
114
|
+
* Son nom porte l'IDENTIFIANT (cf {@link authStateName}) : un état est la session
|
|
115
|
+
* de quelqu'un, et le réutiliser pour un autre compte fait mesurer une identité
|
|
116
|
+
* qu'on n'a pas demandée. Effet de bord bienvenu — deux comptes gardent chacun
|
|
117
|
+
* leur session, donc aucun des deux ne se reconnecte à cause de l'autre.
|
|
118
|
+
*/
|
|
119
|
+
const STATE = path.join(OUT, authStateName(process.env.NF_BROWSER_USER));
|
|
120
|
+
// Créé AVANT la première écriture : en local, le dossier n'existe pas encore,
|
|
121
|
+
// et l'échec ne surviendrait qu'à la sauvegarde — après la connexion, donc
|
|
122
|
+
// après avoir fait croire que tout allait bien.
|
|
123
|
+
mkdirSync(OUT, { recursive: true });
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Le schéma de couleurs à émuler, et le stockage à poser avant chargement.
|
|
127
|
+
*
|
|
128
|
+
* Deux leviers plutôt qu'un, parce qu'il existe deux façons de choisir un
|
|
129
|
+
* thème et qu'aucune ne couvre l'autre :
|
|
130
|
+
*
|
|
131
|
+
* • `prefers-color-scheme` — la média query standard, que le navigateur
|
|
132
|
+
* expose et que le CSS peut suivre. Générique par construction : elle ne
|
|
133
|
+
* dépend d'aucune trousse d'interface.
|
|
134
|
+
* • une entrée de stockage — dès que l'application MÉMORISE le choix de
|
|
135
|
+
* l'utilisateur, la média query ne décide plus rien, et la clé employée
|
|
136
|
+
* appartient à l'application. On la reçoit, on ne la devine pas.
|
|
137
|
+
*
|
|
138
|
+
* Un défaut qui n'existe que dans un thème est invisible tant qu'on ne peut pas
|
|
139
|
+
* demander l'autre : c'est ce qui a fait passer un menu à 1,63:1 sous le radar.
|
|
140
|
+
*/
|
|
141
|
+
const { schema: COLOR_SCHEME, invalid: invalidScheme } = parseColorScheme(
|
|
142
|
+
process.env.NF_BROWSER_COLOR_SCHEME,
|
|
143
|
+
);
|
|
144
|
+
if (invalidScheme) {
|
|
145
|
+
console.error(
|
|
146
|
+
`NF_BROWSER_COLOR_SCHEME inconnu : « ${invalidScheme} ».\n` +
|
|
147
|
+
"Valeurs acceptées (celles de la média query standard) : light, dark, no-preference.",
|
|
148
|
+
);
|
|
149
|
+
process.exit(64); // EX_USAGE
|
|
150
|
+
}
|
|
151
|
+
const { entries: STORAGE, rejected: storageRejected } = parseStorage(
|
|
152
|
+
process.env.NF_BROWSER_STORAGE,
|
|
153
|
+
);
|
|
154
|
+
if (storageRejected.length > 0) {
|
|
155
|
+
console.error(
|
|
156
|
+
`NF_BROWSER_STORAGE — entrée(s) malformée(s) ignorable(s) en silence, donc REFUSÉE(S) : ${storageRejected.join(", ")}\n` +
|
|
157
|
+
"Forme attendue : clé=valeur, séparées par des virgules.",
|
|
158
|
+
);
|
|
159
|
+
process.exit(64); // EX_USAGE
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Ouvre un navigateur et un contexte prêts à mesurer.
|
|
164
|
+
*
|
|
165
|
+
* @returns `{ browser, ctx, page, reuse }` — `reuse` dit si un état
|
|
166
|
+
* d'authentification a été repris, ce qui décide s'il faut se connecter.
|
|
167
|
+
*/
|
|
168
|
+
export async function open() {
|
|
169
|
+
// On CONSTATE quel navigateur répond, au lieu de supposer lequel est là.
|
|
170
|
+
//
|
|
171
|
+
// `channel` demande un navigateur COMPLET plutôt que le
|
|
172
|
+
// `chrome-headless-shell` que le pilote lance par défaut sans interface — une
|
|
173
|
+
// image de conteneur n'embarque souvent que le premier, et sur un poste c'est
|
|
174
|
+
// lui qui rend le plus fidèlement ce qu'un utilisateur verra.
|
|
175
|
+
//
|
|
176
|
+
// L'ordre essaie d'abord celui que le pilote installe, puis ceux DÉJÀ posés
|
|
177
|
+
// sur la machine : la plupart des postes n'ont alors rien à télécharger.
|
|
178
|
+
let browser = null;
|
|
179
|
+
const failures = [];
|
|
180
|
+
for (const channel of BROWSERS) {
|
|
181
|
+
try {
|
|
182
|
+
browser = await chromium.launch({
|
|
183
|
+
channel: channel,
|
|
184
|
+
args: ["--no-sandbox"],
|
|
185
|
+
});
|
|
186
|
+
browserUsed = channel;
|
|
187
|
+
break;
|
|
188
|
+
} catch (e) {
|
|
189
|
+
failures.push(` ${channel} — ${String(e).split("\n")[0].slice(0, 120)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!browser) {
|
|
193
|
+
console.error(
|
|
194
|
+
`Aucun navigateur utilisable parmi : ${BROWSERS.join(", ")}.\n\n` +
|
|
195
|
+
`${failures.join("\n")}\n\n` +
|
|
196
|
+
"Installer celui du pilote (une fois par machine, partagé par tous tes projets) :\n\n" +
|
|
197
|
+
" npx playwright install chromium\n\n" +
|
|
198
|
+
"Ou viser un navigateur déjà présent : NF_BROWSER_ENGINE=chrome (ou msedge).",
|
|
199
|
+
);
|
|
200
|
+
process.exit(69); // EX_UNAVAILABLE
|
|
201
|
+
}
|
|
202
|
+
const options = {
|
|
203
|
+
ignoreHTTPSErrors: true, // certificat de développement auto-signé
|
|
204
|
+
viewport: { width: 1440, height: 900 },
|
|
205
|
+
...(COLOR_SCHEME ? { colorScheme: COLOR_SCHEME } : {}),
|
|
206
|
+
};
|
|
207
|
+
let reuse = Boolean(USER) && existsSync(STATE);
|
|
208
|
+
let ctx = null;
|
|
209
|
+
if (reuse) {
|
|
210
|
+
try {
|
|
211
|
+
ctx = await browser.newContext({ ...options, storageState: STATE });
|
|
212
|
+
} catch (e) {
|
|
213
|
+
// Un fichier d'état corrompu (tronqué, schéma inattendu) ne doit jamais
|
|
214
|
+
// valoir un crash : on le dit, on le jette, et on rejoue le parcours de
|
|
215
|
+
// connexion complet — l'ignorance ne passe pas pour un contrôle réussi.
|
|
216
|
+
console.error(
|
|
217
|
+
`État d'authentification illisible — il est supprimé et le parcours de connexion est rejoué.\n${String(e).slice(0, 200)}`,
|
|
218
|
+
);
|
|
219
|
+
rmSync(STATE, { force: true });
|
|
220
|
+
reuse = false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!ctx) ctx = await browser.newContext(options);
|
|
224
|
+
if (STORAGE.length > 0) {
|
|
225
|
+
// AVANT tout script de la page, et à CHAQUE navigation : une application
|
|
226
|
+
// lit son thème mémorisé au tout premier rendu. Poser la valeur après coup
|
|
227
|
+
// obligerait à recharger, et l'on mesurerait l'entre-deux.
|
|
228
|
+
//
|
|
229
|
+
// Cela l'emporte volontairement sur un état d'authentification réutilisé
|
|
230
|
+
// qui porterait l'ancienne valeur : ce que la ligne de commande demande
|
|
231
|
+
// prime sur ce qu'une session précédente avait laissé.
|
|
232
|
+
await ctx.addInitScript((entries) => {
|
|
233
|
+
try {
|
|
234
|
+
for (const { cle: key, valeur: value } of entries)
|
|
235
|
+
localStorage.setItem(key, value);
|
|
236
|
+
} catch {
|
|
237
|
+
// Stockage refusé (mode privé, origine opaque) : la sonde continue —
|
|
238
|
+
// le thème sera celui par défaut, et la mesure le DIRA (champ `theme`).
|
|
239
|
+
}
|
|
240
|
+
}, STORAGE);
|
|
241
|
+
}
|
|
242
|
+
return { browser, ctx, page: await ctx.newPage(), reuse };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Connexion par le formulaire, en deux temps (identifiant, puis mot de passe).
|
|
247
|
+
*
|
|
248
|
+
* Cible les champs par leur LIBELLÉ, jamais par un sélecteur CSS : « CSS and
|
|
249
|
+
* XPath are not recommended as the DOM can often change » — une classe de
|
|
250
|
+
* composant suit la bibliothèque, le libellé visible est le contrat avec
|
|
251
|
+
* l'utilisateur. On valide par Entrée plutôt que de viser un bouton, dont le
|
|
252
|
+
* texte varie d'une étape à l'autre.
|
|
253
|
+
*
|
|
254
|
+
* `getByRole("textbox", …)` et non `getByLabel(…)` seul : le champ mot de passe
|
|
255
|
+
* partage souvent son libellé avec le bouton « afficher le mot de passe », et le
|
|
256
|
+
* mode strict de Playwright REFUSE alors d'agir (« resolved to 2 elements »)
|
|
257
|
+
* plutôt que de choisir au hasard.
|
|
258
|
+
*
|
|
259
|
+
* @param page - la page à piloter.
|
|
260
|
+
* @param ctx - son contexte, dont l'état est sauvegardé après succès.
|
|
261
|
+
*/
|
|
262
|
+
export async function signIn(page, ctx) {
|
|
263
|
+
await page.goto(`${BASE}${LOGIN}`, { waitUntil: "domcontentloaded" });
|
|
264
|
+
const id = page.getByRole("textbox", {
|
|
265
|
+
name: /identifiant|utilisateur|username|e-?mail/i,
|
|
266
|
+
});
|
|
267
|
+
const pw = page.getByRole("textbox", {
|
|
268
|
+
name: /mot de passe|password/i,
|
|
269
|
+
});
|
|
270
|
+
// Un écran de connexion en deux étapes peut MÉMORISER l'identifiant (stockage
|
|
271
|
+
// local) et présenter directement le mot de passe : exiger l'étape 1 faisait
|
|
272
|
+
// expirer la sonde sur un parcours parfaitement sain. On attend la PREMIÈRE
|
|
273
|
+
// des deux étapes qui se présente, et on ne remplit l'identifiant que si son
|
|
274
|
+
// champ existe.
|
|
275
|
+
// Aucun champ trouvé : dire CE QU'ON A CHERCHÉ et où, plutôt que de laisser
|
|
276
|
+
// remonter un dépassement de délai brut. Les deux causes réelles sont
|
|
277
|
+
// banales — le chemin donné n'est pas un écran de connexion (une page
|
|
278
|
+
// d'erreur en rend un 404 tout aussi silencieux), ou les libellés du
|
|
279
|
+
// formulaire ne sont pas ceux qu'on reconnaît. L'un et l'autre se corrigent
|
|
280
|
+
// en une seconde quand on les lit, et coûtent un quart d'heure sinon.
|
|
281
|
+
try {
|
|
282
|
+
await id.or(pw).first().waitFor({ timeout: 15000 });
|
|
283
|
+
} catch {
|
|
284
|
+
const title = await page.title().catch(() => "");
|
|
285
|
+
console.error(
|
|
286
|
+
`Aucun champ de connexion trouvé sur ${BASE}${LOGIN}\n` +
|
|
287
|
+
` page réellement ouverte : ${page.url()}${title ? ` (« ${title} »)` : ""}\n\n` +
|
|
288
|
+
"Deux causes, à vérifier dans cet ordre :\n" +
|
|
289
|
+
" 1. ce chemin n'est pas ton écran de connexion — une route absente rend\n" +
|
|
290
|
+
" une page d'erreur, où la sonde attendrait indéfiniment ;\n" +
|
|
291
|
+
" 2. tes champs ne portent pas les libellés reconnus (identifiant,\n" +
|
|
292
|
+
" utilisateur, e-mail / mot de passe) — la sonde vise le LIBELLÉ\n" +
|
|
293
|
+
" visible, pas un sélecteur, parce que c'est lui le contrat avec\n" +
|
|
294
|
+
" l'utilisateur. Ajoute un `aria-label` si ton champ n'en a pas.",
|
|
295
|
+
);
|
|
296
|
+
process.exit(65); // EX_DATAERR — l'écran attendu n'est jamais apparu
|
|
297
|
+
}
|
|
298
|
+
if ((await id.count()) > 0) {
|
|
299
|
+
await id.fill(USER);
|
|
300
|
+
await id.press("Enter");
|
|
301
|
+
}
|
|
302
|
+
await pw.fill(PASSWORD, { timeout: 15000 });
|
|
303
|
+
await pw.press("Enter");
|
|
304
|
+
await page.waitForURL((u) => !u.pathname.endsWith(LOGIN), { timeout: 20000 });
|
|
305
|
+
await ctx.storageState({ path: STATE });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Ouvre la page demandée, en garantissant que c'est bien ELLE qu'on mesure.
|
|
310
|
+
*
|
|
311
|
+
* Un état d'authentification réutilisé peut être PÉRIMÉ (session expirée,
|
|
312
|
+
* serveur redémarré, magasin vidé) : l'application renvoie alors sur le
|
|
313
|
+
* formulaire, et l'on mesurerait l'écran de connexion en croyant tenir la page
|
|
314
|
+
* demandée. On le constate et on refait le parcours plutôt que de rendre une
|
|
315
|
+
* mesure fausse.
|
|
316
|
+
*
|
|
317
|
+
* @param page - la page à piloter.
|
|
318
|
+
* @param ctx - son contexte.
|
|
319
|
+
* @param pathname - le chemin à ouvrir, relatif à l'origine.
|
|
320
|
+
* @param reuse - un état d'authentification a-t-il été repris au lancement.
|
|
321
|
+
*/
|
|
322
|
+
export async function goTo(page, ctx, pathname, reuse) {
|
|
323
|
+
if (USER && !reuse) await signIn(page, ctx);
|
|
324
|
+
await page.goto(`${BASE}${pathname}`, { waitUntil: "domcontentloaded" });
|
|
325
|
+
if (USER && reuse) {
|
|
326
|
+
// 🔴 L'URL au `domcontentloaded` MENT sur une application à rendu client :
|
|
327
|
+
// le serveur répond 200 sur toutes les routes, et c'est le code de la
|
|
328
|
+
// page qui, une fois monté, constate la session invalide et renvoie vers
|
|
329
|
+
// le formulaire. Tester l'URL immédiatement laissait donc passer TOUT
|
|
330
|
+
// état périmé — la sonde restait sur l'écran de connexion et concluait
|
|
331
|
+
// « identifiants refusés » sur des identifiants valides. On accorde à ce
|
|
332
|
+
// détour le temps d'avoir lieu ; la fenêtre couvre aussi la redirection
|
|
333
|
+
// serveur (déjà sur le formulaire ⇒ résolue immédiatement), et ne se paie
|
|
334
|
+
// que sur une session REPRISE — jamais après une connexion fraîche.
|
|
335
|
+
const redirected = await page
|
|
336
|
+
.waitForURL((u) => u.pathname.endsWith(LOGIN), { timeout: 3000 })
|
|
337
|
+
.then(
|
|
338
|
+
() => true,
|
|
339
|
+
() => false,
|
|
340
|
+
);
|
|
341
|
+
if (redirected) {
|
|
342
|
+
// Repartir d'un contexte VIERGE avant de rejouer le parcours — cookies
|
|
343
|
+
// ET stockage web. Les cookies : le jeton anti-CSRF est lié à la session
|
|
344
|
+
// morte, la garder fait refuser la soumission et accuser le mot de
|
|
345
|
+
// passe. Le stockage : l'application peut y avoir MÉMORISÉ l'identifiant
|
|
346
|
+
// et présenter un formulaire raccourci — voire connecter un AUTRE
|
|
347
|
+
// utilisateur que celui demandé. Vécu, les deux.
|
|
348
|
+
await ctx.clearCookies();
|
|
349
|
+
await page.evaluate(() => {
|
|
350
|
+
localStorage.clear();
|
|
351
|
+
sessionStorage.clear();
|
|
352
|
+
});
|
|
353
|
+
await signIn(page, ctx);
|
|
354
|
+
await page.goto(`${BASE}${pathname}`, { waitUntil: "domcontentloaded" });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|