Migration vers nukadoko
À quoi ça sert
Une suite existante qui migre vers nukadoko, morceau par morceau — pas une réécriture complète. Le point de départ diffère :
- Une suite cucumber-js — fichiers feature plus glue, utilisant généralement Playwright.
- Une suite déjà structurée en étapes typées — son propre DSL, pas celui de cucumber, mais suffisamment proche de
defineStepde nukadoko pour que la migration soit surtout une traduction.
Dans les deux cas, la migration se fait par étapes, jamais d'un coup ; le reste de cette compétence explique pourquoi, puis décrit chaque point de départ tour à tour.
Tout ce qui suit suppose que le projet est déjà initialisé. Si ce n'est pas encore le cas (pas de nukadoko.config.ts), exécutez d'abord nuka init.
Deux étapes, jamais en même temps
Modifiez une chose à la fois, puis la suivante — jamais les deux en même temps. Si une étape échoue après deux changements appliqués ensemble, il est impossible de savoir lequel l'a cassée : les causes sont mélangées dans un seul échec. Diviser le travail en étapes permet de relier chaque échec à exactement un changement. C'est tout l'intérêt — les étapes elles-mêmes sont secondaires par rapport à ce principe.
Si les étapes ci-dessous ne correspondent pas à votre point de départ, dérivez vos propres deux étapes à partir de ce principe. Ce qui doit être garanti, c'est la traçabilité, pas ces noms d'étapes particuliers.
Deux applications suivent : une suite cucumber-js se divise en « changer l'import » et « ajouter le typage » (ci-dessous) ; une suite déjà structurée en étapes typées se divise différemment, décrite plus loin.
En venant d'une suite cucumber-js
C'est le cas auquel les deux étapes ci-dessus font référence.
Étape 1 — la faire fonctionner
Changez l'import unique que chaque fichier de glue utilise :
// avant : import { Given, When, Then } from "@cucumber/cucumber";
import { Given, When, Then } from "nukadoko/compat";
L'extension détermine ce que la découverte lit, pas l'historique de la suite : les fichiers glue .ts, .mts, .js, et .mjs sont tous lus de la même manière, donc une suite qui a toujours été en JavaScript pur n'a pas besoin d'être réécrite en TypeScript pour commencer cette étape. Un fichier .cjs est la seule exception : nukadoko est ESM uniquement, donc la découverte n'en importe jamais un ; nuka check le nomme à la place (step-file-unsupported-extension) plutôt que de le laisser disparaître comme une undefined-step inexpliquée. Le renommer en .js n'aide que si le code du fichier utilise déjà la syntaxe des modules ES ; s'il utilise encore require(...), cela échoue au même import pour la même raison ESM, et nuka check le signale comme step-file-import-failed.
Exécutez ensuite nuka check <feature> et nuka run <feature> et lisez ce qu'ils disent. Corrigez ce qu'ils signalent, et relancez-les. Recommencez jusqu'à ce que la suite existante soit verte — c'est la condition de fin de l'Étape 1, rien de plus. Cette condition est que nuka check et nuka run soient verts, pas que tsc typecheck passe proprement ; les deux peuvent diverger dans un sens ou dans l'autre.
nuka steps et nuka describe restent utilisables tout au long de l'Étape 1, même si certains fichiers de glue échouent encore à l'import : les deux lisent les fichiers d'étapes un par un, donc un fichier qui échoue encore à l'import est nommé (import_failures sur --json, stderr sinon) au lieu de vider tout le vocabulaire. Lisez le vocabulaire au fur et à mesure plutôt que d'attendre que tous les fichiers s'importent proprement d'abord ; nuka run <feature> et nuka do <step> refusent encore catégoriquement un fichier de glue illisible, intentionnellement, puisqu'ils sont sur le point d'exécuter.
Ne cherchez pas une liste de ce que compat ne prend pas en charge avant de commencer. Tout ce qui ne fonctionnera pas échoue bruyamment, soit à l'import, soit au premier nuka run — l'échec nomme ce qui a cassé, et c'est sur quoi agir. Une liste écrite ici deviendrait obsolète dès que la couverture de compat s'agrandit ; la sortie du CLI, elle, ne vieillit jamais.
Étape 2 — lui donner des contrats
Une fois que la suite fonctionne, certaines étapes méritent d'être typées. Voici ce qui change quand on en type une :
| étape compat | étape typée | |
|---|---|---|
| Entrée | capture de motif uniquement, non vérifiée | validée contre un schéma args, chaque champ portant un .describe() |
| Sortie | ignorée — le result du reçu est null | validée contre un schéma returns et stockée dans le reçu |
| Dépendances | effets de bord sur le World, invisibles dans la signature de la fonction | déclarées avec from, nommées dans un import, vérifiées par nuka check avant toute exécution, et enregistrées comme used dans le reçu |
| Mot-clé | décoratif — une étape liée à Then peut toujours muter | mutates est une déclaration que nukadoko prend pour argent comptant : déclarez mutates: true et un environnement en lecture seule refuse de l'exécuter, et nuka check avertit si elle est liée à Then ; ce qui a réellement été exécuté est toujours enregistré dans les compteurs observed du reçu |
| Exécution seule | impossible (le World est vide en dehors d'un scénario) | nuka do <step> l'exécute directement, reçu imprimé sur stdout ; une clé from provient de --args comme toute autre, ou de --use <receipt-id> pour une clé tirée d'une exécution antérieure ; un appel de fixture resultOf à l'intérieur de run ne trouve toujours rien, puisqu'il n'y a pas de scénario à parcourir |
Cette dernière ligne est un fait distinct de la ligne « Dépendances » ci-dessus, pas une conséquence : une étape compat ne peut pas être exécutée seule parce que ce dont elle a besoin vit sur un World que rien n'a encore peuplé, et le World ne fait pas partie de sa signature — il n'y a rien à inspecter pour savoir quoi configurer en premier. Les dépendances d'une étape typée sont nommées comme des entrées from, visibles dans un import, donc un appel nuka do peut les fournir manuellement : --args pour une clé ordinaire, ou --use <receipt-id> pour une clé tirée du résultat d'une exécution antérieure — le nom de l'étape en amont n'a jamais à apparaître sur la ligne de commande, car le reçu cité le porte déjà. Une étape dont toutes les clés arrivent de cette manière n'a besoin d'aucun --args : --use seul est une invocation complète. Ce qui ne trouve toujours rien en dehors d'un scénario, c'est une dépendance lue via la fixture resultOf depuis l'intérieur de run : cet appel n'a pas de chaîne à parcourir quand il n'y avait pas de scénario pour en construire une, --use ou pas.
Promouvez d'abord les étapes que vous exécutez le plus souvent, une par une — pas toute la suite en une seule passe. La façon de réécrire un morceau de glue donné est une décision à prendre au moment où vous le faites ; nukadoko ne prescrit pas une recette unique, et cette compétence non plus.
La rédaction du run d'une étape typée aboutit parfois à la forme non migrée run(ctx, args) avant d'être complètement déstructurée. nuka steps --json nomme déjà les fixtures que cette ébauche touche, comme needs_inferred : une estimation lexicale lue à partir des accès aux membres de ctx, accompagnée de needs: null et d'un needs_error expliquant pourquoi le vrai contrat ne peut pas encore être lu. Cela évite de relire tout le corps à la main pour savoir quoi ajouter à la signature déstructurée, mais c'est une estimation, pas le contrat : elle manque un alias (const c = ctx) et ne nomme jamais needs_browser, donc terminez la déstructuration plutôt que de la considérer comme terminée.
À quoi ressemble une étape promue
Un exemple. Avant, glue compat :
const { Given } = require("@cucumber/cucumber");
Given("un projet {string} existe", async function (name) {
const res = await this.request.post("/projects", { data: { name } });
this.projectId = (await res.json()).id;
});
Après, une étape typée (features/steps/create-project.ts) :
import { defineStep } from "nukadoko";
import { z } from "zod";
export default defineStep({
pattern: "un projet {name:string} existe",
description: "Créer un projet et retourner son id",
args: z.object({
name: z.string().describe("le nom visible du projet"),
}),
returns: z.object({
id: z.string().describe("l'id du projet créé, pour que les étapes suivantes le citent"),
}),
mutates: true,
async run({ request }, args) {
const res = await request.post("/projects", { data: args });
return res.json();
},
});
Ce qui a changé :
this.projectIda disparu ; l'id revient viareturnsà la place. Une étape ultérieure déclarefrom: { projectId: [createProjectStep, "id"] }pour le lire par clé —nuka checkvérifie l'ordre de liaison avant toute exécution, et la lecture apparaît dans le reçu de cette étape ultérieure (la fixtureresultOfreste disponible pour les lectures plus rares qu'un nom de clé ne peut pas exprimer).- L'argument est une capture nommée liée à une clé de schéma (
{name:string}→args.name), donc le seul motif montre quel texte devient quel champ. mutates: trueest maintenant une déclaration, pas seulement un fait sur ce que le code fait par hasard —nuka checkavertit si cette étape est jamais liée àThen, et un environnement en lecture seule refuse de l'exécuter complètement. Le reçu enregistre toujours ce que l'exécution a réellement envoyé, pour révision, mais ce compteur n'entraîne pas le rejet de l'étape.this.requestdevient la fixturerequest, nommée par déstructuration du premier argument : seules les fixtures que l'exécuteur injecte réellement peuvent être nommées là, rien d'implicite, et seuls les noms réellement nommés sont construits. Une étape qui ne nomme nipagenicontextne lance jamais de navigateur.
C'est le seul exemple travaillé ici. Ce n'est pas un catalogue de toutes les différences entre compat et typé — si un autre motif revient assez souvent pour en mériter un, c'est un ajout séparé, pas quelque chose à improviser à partir de ce seul cas.
Quand un hook global After était le nettoyage
Une suite cucumber-js s'appuie souvent sur un hook global After pour nettoyer ce qu'une étape a créé, un locataire, une ligne ensemencée, un fichier temporaire. Cela mérite d'être revu une fois que l'étape qui l'a créé est promue : déclarez la ressource comme une fixture sous les fixtures propres à nukadoko.config.ts à la place, avec le nettoyage écrit juste après await use(...) dans la même fonction qui l'a construite, plutôt que laissé dans un hook qui doit deviner, à partir des tags seuls, quels scénarios en ont réellement besoin. defineFixtures (du paquet nukadoko) garde la fixture entièrement typée ; toute étape typée y accède de la même manière qu'elle accède à page ou request, en déstructurant le nom. C'est un ajout, pas une obligation : un hook After qui nettoie quelque chose qu'aucune étape typée n'a encore réclamé est toujours exactement aussi valable qu'il l'était à l'Étape 1.
En venant d'un DSL structuré en étapes typées
S'il n'y a pas de fichiers feature et pas de glue cucumber, la porte compat de l'Étape 1 ci-dessus n'est pas pertinente — sautez directement au-delà.
Ce qui rend cela possible, c'est que pattern est optionnel sur defineStep : une étape peut être définie sans motif du tout et être néanmoins un morceau complet de vocabulaire uniquement CLI, exécutable avec nuka do et inspectable avec nuka describe. C'est une paire d'étapes différente produite par le même principe ci-dessus : déplacez d'abord chaque étape vers un defineStep typé, et regroupez-la dans un pattern Gherkin plus tard, chaque fois qu'un fichier feature rend cela utile.
Si le DSL source porte déjà quelque chose comme un description, args, returns, mutates, et une fonction run, la traduction vers defineStep est directe — chacun a un équivalent dans defineStep pour le recevoir.
Ce qu'il ne faut pas faire
- Ne faites pas l'Étape 1 et l'Étape 2 en même temps. Mélanger un changement d'import avec un typage ajouté dans le même changement est exactement ce qui rend un échec non attribuable — voir « Deux étapes, jamais en même temps » ci-dessus.
- Ne laissez pas la suite cesser de fonctionner pendant qu'elle est partiellement migrée. Les étapes compat et typées coexistent dans le même fichier feature ; une suite avec certaines étapes promues et d'autres encore compat doit continuer de passer tout au long. Ne faites jamais de « entièrement typé » une condition préalable à « fonctionne ».
- Ne supprimez pas la glue compat avant que son remplacement typé soit écrit et fonctionnel. Écrivez la nouvelle étape, faites-la fonctionner avec
nuka do, puis seulement retirez l'ancienne — jamais dans l'autre ordre. - Ne devinez pas une correction que
nuka checkounuka runn'a pas demandée. Leur sortie est la preuve de ce qui ne va pas ; modifier quelque chose qu'ils n'ont pas signalé est un changement fait sur une intuition, pas sur ce qui a réellement cassé.