@mostajs/mjs-unit 0.2.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ @mostajs/mjs-unit
2
+ Copyright (C) 2026 Dr Hamid MADANI <drmdh@msn.com>
3
+
4
+ SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+
19
+ The complete text of the GNU Affero General Public License version 3 is
20
+ available at the URL above and must accompany any distribution of this
21
+ software.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @mostajs/mjs-unit
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later
4
+
5
+ Micro-framework de test **zéro dépendance** pour l'écosystème `@mostajs/*`. Trois apports :
6
+
7
+ 1. **API minimale** : `test()`, `describe()`, `expect()` + assertions (`ok`, `equal`, `deepEqual`, `throwsAsync`, `doesNotThrowAsync`).
8
+ 2. **Runner CLI** (`mjs-unit <fichiers>`) : importe les fichiers de test, exécute, affiche un rapport, code de sortie ≠ 0 si échec.
9
+ 3. **Resolver ESM** : exécute sous Node les `dist/` à imports **sans extension** (convention `moduleResolution: bundler` des modules @mostajs) — sans ce resolver, `node` échoue sur `import './x'`.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm i -D @mostajs/mjs-unit
15
+ # module local non publié : npm i -D "@mostajs/mjs-unit@file:../chemin/mosta-mjs-unit"
16
+ ```
17
+
18
+ ## How to use (pas à pas, dans n'importe quelle application)
19
+
20
+ 1. **Installer** le module en devDependency (voir ci-dessus).
21
+ 2. **Créer** un dossier `test-scripts/` et y écrire des fichiers `*.test.mjs`
22
+ (un test = `test(name, fn)` ; ne pas appeler `run()`, le runner s'en charge).
23
+ 3. **Versionner** un runner `test-scripts/run-tests.sh` qui (re)bâtit puis lance le CLI.
24
+ 4. **Brancher** `package.json` : `"scripts": { "test": "bash test-scripts/run-tests.sh" }`.
25
+ 5. **Lancer** : `npm test` (code de sortie ≠ 0 si un test échoue → utilisable en CI).
26
+
27
+ ```js
28
+ // test-scripts/api.test.mjs — test d'unité + test web, dans la même suite
29
+ import { test, equal, ok } from "@mostajs/mjs-unit";
30
+ import { createHttpTester, waitForEvent } from "@mostajs/mjs-unit/web";
31
+ import { app } from "../dist/app.js"; // votre app Express / handler natif
32
+
33
+ const api = createHttpTester(app);
34
+
35
+ test("GET /healthz → 200", async () => {
36
+ const res = await api.get("/healthz");
37
+ equal(res.status, 200);
38
+ ok(res.body.status === "ok");
39
+ });
40
+ ```
41
+
42
+ ```bash
43
+ # test-scripts/run-tests.sh
44
+ #!/usr/bin/env bash
45
+ set -euo pipefail
46
+ cd "$(dirname "$0")/.."
47
+ npm run build >/dev/null
48
+ node node_modules/@mostajs/mjs-unit/bin/cli.mjs test-scripts/*.test.mjs
49
+ ```
50
+
51
+ > Besoin d'un profil d'environnement (DB de test, secrets…) avant d'importer le
52
+ > `dist/` ? Fixez-le **avant** les imports via un import dynamique :
53
+ > `process.env.APP_ENV = "TEST"; const { app } = await import("../dist/app.js");`
54
+ > (les `import` statiques sont hoistés, donc exécutés avant le code du module).
55
+
56
+ ## Écrire un test (`test-scripts/mon.test.mjs`)
57
+
58
+ ```js
59
+ import { test, expect, ok } from "@mostajs/mjs-unit";
60
+ import { maFonction } from "../dist/lib/ma-fonction.js"; // le dist peut avoir des imports sans extension
61
+
62
+ test("addition", () => expect(maFonction(2, 2)).toBe(4));
63
+ test("async", async () => { ok(await maFonction.async()); });
64
+ ```
65
+
66
+ > Ne pas appeler `run()` dans le fichier : le **runner** s'en charge (les cas enregistrés via `test()` sont exécutés groupés par fichier).
67
+
68
+ ## Lancer
69
+
70
+ ```bash
71
+ mjs-unit test-scripts/*.test.mjs # via le bin
72
+ # ou, programmatique :
73
+ node --import @mostajs/mjs-unit/register test-scripts/mon.test.mjs # + appeler run() soi-même
74
+ ```
75
+
76
+ Runner versionné conseillé (`test-scripts/run-tests.sh`) :
77
+
78
+ ```bash
79
+ #!/usr/bin/env bash
80
+ set -euo pipefail
81
+ cd "$(dirname "$0")/.."
82
+ npm run build >/dev/null
83
+ node node_modules/@mostajs/mjs-unit/bin/cli.mjs test-scripts/*.test.mjs
84
+ ```
85
+
86
+ ## API
87
+
88
+ | Fonction | Rôle |
89
+ |---|---|
90
+ | `test(name, fn)` / `test.skip(...)` | Enregistre un cas (sync ou async) |
91
+ | `describe(group, body)` | Préfixe les cas du groupe |
92
+ | `expect(v).toBe/toEqual/toBeTruthy/toBeFalsy/toContain` | Assertions fluides |
93
+ | `ok/equal/deepEqual(...)` | Assertions directes |
94
+ | `throwsAsync/doesNotThrowAsync(fn)` | Vérifie qu'une promesse (ne) lève (pas) |
95
+ | `run(label?)` | Exécute les cas en attente → `{passed, failed, skipped}` (appelé par le CLI) |
96
+
97
+ Zéro dépendance runtime ; `@types/node` en dev uniquement.
98
+
99
+ ## Tests web (`@mostajs/mjs-unit/web`)
100
+
101
+ Moteur **générique** pour tester des applications HTTP et événementielles — sans
102
+ `supertest`, `jest` ni client socket dans le framework. Toujours **zéro
103
+ dépendance** (uniquement `node:http`/`node:net`).
104
+
105
+ | Fonction | Rôle |
106
+ |---|---|
107
+ | `startServer(target)` | Démarre un serveur HTTP éphémère (port 0) autour d'un `http.Server`, d'un `RequestListener` ou d'une app Express → `{ url, port, close() }` |
108
+ | `createHttpTester(target)` | Client HTTP façon supertest : `get/post/put/del/request` → `{ status, headers, body }`. `target` = URL d'un serveur démarré **ou** handler/serveur (éphémère par requête) |
109
+ | `waitForEvent(emitter, name, { timeout })` | Attend un événement sur tout émetteur (`EventEmitter`, client Socket.io, `ws`…) — teste le temps réel |
110
+
111
+ ```js
112
+ import { test, equal, ok } from "@mostajs/mjs-unit";
113
+ import { createHttpTester, startServer, waitForEvent } from "@mostajs/mjs-unit/web";
114
+
115
+ // HTTP : éphémère par requête (app Express ou handler natif (req,res)=>…)
116
+ const api = createHttpTester(app);
117
+ test("login", async () => {
118
+ const res = await api.post("/login", { username: "admin", password: "admin" });
119
+ equal(res.status, 200);
120
+ ok(res.body.token);
121
+ });
122
+
123
+ // Temps réel : serveur durable + attente d'événement (ex. Socket.io)
124
+ test("push", async () => {
125
+ const srv = await startServer(httpServer); // serveur partagé
126
+ const client = ioClient(srv.url); // socket.io-client fourni par l'app
127
+ const received = waitForEvent(client, "new_ticket");
128
+ await createHttpTester(srv.url).post("/api/tickets", { serviceType: "general" });
129
+ const evt = await received;
130
+ ok(evt.ticket);
131
+ client.close();
132
+ await srv.close();
133
+ });
134
+ ```
135
+
136
+ > Le client socket (`socket.io-client`, `ws`…) reste fourni par l'application :
137
+ > mjs-unit ne l'embarque pas, pour préserver le zéro-dépendance. `waitForEvent`
138
+ > fonctionne avec n'importe quel émetteur exposant `on` (idéalement `once`/`off`).
package/bin/cli.mjs ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @mostajs/mjs-unit — runner CLI. Active le resolver (imports sans extension), importe les fichiers
4
+ * de test passés en argument (chacun enregistre ses cas via `test()`), puis exécute.
5
+ * Usage : mjs-unit <fichier-de-test...> (ex. mjs-unit test-scripts/*.test.mjs)
6
+ * @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
7
+ */
8
+ import { register } from "node:module";
9
+ import { pathToFileURL } from "node:url";
10
+
11
+ register(new URL("../loader.mjs", import.meta.url));
12
+ const { run, pending } = await import("../dist/index.js");
13
+
14
+ const files = process.argv.slice(2);
15
+ if (!files.length) { console.error("usage: mjs-unit <fichier-de-test...>"); process.exit(2); }
16
+
17
+ let totalFail = 0;
18
+ for (const f of files) {
19
+ await import(pathToFileURL(f).href); // le fichier enregistre ses cas (et peut appeler run() lui-même)
20
+ if (pending() > 0) { // sinon, on exécute les cas restants groupés par fichier
21
+ const r = await run(f);
22
+ totalFail += r.failed;
23
+ }
24
+ }
25
+ if (totalFail > 0) process.exitCode = 1;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @mostajs/mjs-unit — micro-framework de test (zéro dépendance).
3
+ * API : `test(name, fn)` enregistre un cas ; `expect(v)` / helpers d'assertion ; `run()` exécute,
4
+ * affiche un rapport et positionne `process.exitCode`. Pensé pour l'écosystème @mostajs/* (ESM pur).
5
+ *
6
+ * @author Dr Hamid MADANI <drmdh@msn.com>
7
+ * @license AGPL-3.0-or-later
8
+ */
9
+ export interface TestCase {
10
+ name: string;
11
+ fn: () => void | Promise<void>;
12
+ skip?: boolean;
13
+ }
14
+ /** Enregistre un test (exécuté par `run()`). */
15
+ export declare function test(name: string, fn: () => void | Promise<void>): void;
16
+ export declare namespace test {
17
+ var skip: (name: string, fn: () => void | Promise<void>) => void;
18
+ }
19
+ /** Groupe logique (préfixe les noms). */
20
+ export declare function describe(group: string, body: () => void): void;
21
+ export declare function ok(cond: unknown, message?: string): void;
22
+ export declare function equal(actual: unknown, expected: unknown, message?: string): void;
23
+ export declare function deepEqual(actual: unknown, expected: unknown, message?: string): void;
24
+ export declare function throwsAsync(fn: () => Promise<unknown>, message?: string): Promise<void>;
25
+ export declare function doesNotThrowAsync(fn: () => Promise<unknown>, message?: string): Promise<void>;
26
+ /** API fluide : `expect(x).toBe(y)`, `.toEqual(obj)`, `.toBeTruthy()`, `.toContain(s)`. */
27
+ export declare function expect(actual: unknown): {
28
+ toBe: (e: unknown, m?: string) => void;
29
+ toEqual: (e: unknown, m?: string) => void;
30
+ toBeTruthy: (m?: string) => void;
31
+ toBeFalsy: (m?: string) => void;
32
+ toContain: (sub: string, m?: string) => void;
33
+ };
34
+ export interface RunResult {
35
+ passed: number;
36
+ failed: number;
37
+ skipped: number;
38
+ }
39
+ /** Exécute les tests enregistrés, affiche le rapport, positionne `process.exitCode` si échec. */
40
+ export declare function run(label?: string): Promise<RunResult>;
41
+ /** Nombre de tests en attente (diagnostic). */
42
+ export declare function pending(): number;
43
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,QAAQ;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE;AAI1F,gDAAgD;AAChD,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAEvE;yBAFe,IAAI;qBAID,MAAM,MAAM,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAAI;;AAEhE,yCAAyC;AACzC,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,IAAI,GAAG,IAAI,CAI9D;AAMD,wBAAgB,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,SAAiB,GAAG,IAAI,CAEhE;AACD,wBAAgB,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAEhF;AACD,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAEpF;AACD,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,SAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAGrG;AACD,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,SAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAElH;AAED,2FAA2F;AAC3F,wBAAgB,MAAM,CAAC,MAAM,EAAE,OAAO;cAExB,OAAO,MAAM,MAAM;iBAChB,OAAO,MAAM,MAAM;qBACf,MAAM;oBACP,MAAM;qBACL,MAAM,MAAM,MAAM;EAEtC;AAGD,MAAM,WAAW,SAAS;IAAG,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAE9E,iGAAiG;AACjG,wBAAsB,GAAG,CAAC,KAAK,SAAK,GAAG,OAAO,CAAC,SAAS,CAAC,CAYxD;AAED,+CAA+C;AAC/C,wBAAgB,OAAO,IAAI,MAAM,CAA0B"}
package/dist/index.js ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @mostajs/mjs-unit — micro-framework de test (zéro dépendance).
3
+ * API : `test(name, fn)` enregistre un cas ; `expect(v)` / helpers d'assertion ; `run()` exécute,
4
+ * affiche un rapport et positionne `process.exitCode`. Pensé pour l'écosystème @mostajs/* (ESM pur).
5
+ *
6
+ * @author Dr Hamid MADANI <drmdh@msn.com>
7
+ * @license AGPL-3.0-or-later
8
+ */
9
+ const _suite = [];
10
+ /** Enregistre un test (exécuté par `run()`). */
11
+ export function test(name, fn) {
12
+ _suite.push({ name, fn });
13
+ }
14
+ /** Test ignoré (compté à part). */
15
+ test.skip = (name, fn) => { _suite.push({ name, fn, skip: true }); };
16
+ /** Groupe logique (préfixe les noms). */
17
+ export function describe(group, body) {
18
+ const start = _suite.length;
19
+ body();
20
+ for (let i = start; i < _suite.length; i++)
21
+ _suite[i].name = `${group} › ${_suite[i].name}`;
22
+ }
23
+ // ── Assertions ───────────────────────────────────────────────────────────────
24
+ class AssertionError extends Error {
25
+ constructor(m) { super(m); this.name = "AssertionError"; }
26
+ }
27
+ const fmt = (v) => { try {
28
+ return typeof v === "string" ? v : JSON.stringify(v);
29
+ }
30
+ catch {
31
+ return String(v);
32
+ } };
33
+ export function ok(cond, message = "valeur falsy") {
34
+ if (!cond)
35
+ throw new AssertionError(message);
36
+ }
37
+ export function equal(actual, expected, message) {
38
+ if (actual !== expected)
39
+ throw new AssertionError(message ?? `attendu ${fmt(expected)}, reçu ${fmt(actual)}`);
40
+ }
41
+ export function deepEqual(actual, expected, message) {
42
+ if (JSON.stringify(actual) !== JSON.stringify(expected))
43
+ throw new AssertionError(message ?? `attendu ${fmt(expected)}, reçu ${fmt(actual)}`);
44
+ }
45
+ export async function throwsAsync(fn, message = "devait lever") {
46
+ try {
47
+ await fn();
48
+ }
49
+ catch {
50
+ return;
51
+ }
52
+ throw new AssertionError(message);
53
+ }
54
+ export async function doesNotThrowAsync(fn, message = "ne devait pas lever") {
55
+ try {
56
+ await fn();
57
+ }
58
+ catch (e) {
59
+ throw new AssertionError(`${message} : ${e?.message}`);
60
+ }
61
+ }
62
+ /** API fluide : `expect(x).toBe(y)`, `.toEqual(obj)`, `.toBeTruthy()`, `.toContain(s)`. */
63
+ export function expect(actual) {
64
+ return {
65
+ toBe: (e, m) => equal(actual, e, m),
66
+ toEqual: (e, m) => deepEqual(actual, e, m),
67
+ toBeTruthy: (m) => ok(actual, m),
68
+ toBeFalsy: (m) => ok(!actual, m ?? "attendu falsy"),
69
+ toContain: (sub, m) => ok(typeof actual === "string" && actual.includes(sub), m ?? `« ${fmt(actual)} » ne contient pas « ${sub} »`),
70
+ };
71
+ }
72
+ /** Exécute les tests enregistrés, affiche le rapport, positionne `process.exitCode` si échec. */
73
+ export async function run(label = "") {
74
+ let passed = 0, failed = 0, skipped = 0;
75
+ if (label)
76
+ console.log(`\n▶ ${label}`);
77
+ for (const t of _suite) {
78
+ if (t.skip) {
79
+ console.log(` ⊘ ${t.name} (ignoré)`);
80
+ skipped++;
81
+ continue;
82
+ }
83
+ try {
84
+ await t.fn();
85
+ console.log(` ✓ ${t.name}`);
86
+ passed++;
87
+ }
88
+ catch (e) {
89
+ console.log(` ✗ ${t.name}\n ${e?.message ?? e}`);
90
+ failed++;
91
+ }
92
+ }
93
+ console.log(`\n${failed === 0 ? "✅" : "❌"} ${passed} passé(s), ${failed} échoué(s)${skipped ? `, ${skipped} ignoré(s)` : ""}`);
94
+ if (failed > 0 && typeof process !== "undefined")
95
+ process.exitCode = 1;
96
+ _suite.length = 0; // réinitialise pour un éventuel run suivant
97
+ return { passed, failed, skipped };
98
+ }
99
+ /** Nombre de tests en attente (diagnostic). */
100
+ export function pending() { return _suite.length; }
101
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/web.d.ts ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @mostajs/mjs-unit/web — extension de test web (zéro dépendance).
3
+ *
4
+ * Moteur générique pour tester des applications HTTP et événementielles sans
5
+ * tirer supertest, jest ou socket.io-client dans le framework lui-même :
6
+ *
7
+ * - `startServer(target)` : démarre un serveur HTTP éphémère (port 0) autour
8
+ * d'un `http.Server`, d'un `RequestListener` ou d'une app Express
9
+ * (`(req,res) => …`). Rend `{ url, port, close() }`. Utile quand le test a
10
+ * besoin d'un serveur durable (ex. connexion WebSocket/Socket.io).
11
+ * - `createHttpTester(target)` : client HTTP générique façon supertest.
12
+ * `target` peut être une URL de base (serveur déjà démarré) OU un
13
+ * handler/serveur (un serveur éphémère est alors ouvert puis fermé à CHAQUE
14
+ * requête). Méthodes `get/post/put/del/request` → `{ status, headers, body }`.
15
+ * - `waitForEvent(emitter, name, opts)` : attend un événement sur n'importe
16
+ * quel émetteur compatible (`EventEmitter`, client Socket.io, `ws`…) avec
17
+ * garde-fou de délai. Permet de tester le temps réel sans dépendance.
18
+ *
19
+ * S'appuie uniquement sur les modules natifs `node:http`/`node:net`. Aucune
20
+ * dépendance ajoutée : conforme à la philosophie zéro-dép de @mostajs/mjs-unit.
21
+ *
22
+ * @author Dr Hamid MADANI <drmdh@msn.com>
23
+ * @license AGPL-3.0-or-later
24
+ */
25
+ import type { IncomingHttpHeaders, Server, RequestListener } from "node:http";
26
+ /** Cible acceptée : serveur HTTP déjà construit, ou handler de requêtes (app Express). */
27
+ export type ServerTarget = Server | RequestListener;
28
+ /** Serveur de test démarré sur un port éphémère. */
29
+ export interface TestServer {
30
+ /** URL de base, ex. `http://127.0.0.1:54123`. */
31
+ url: string;
32
+ /** Port effectif attribué par l'OS. */
33
+ port: number;
34
+ /** Le serveur Node sous-jacent. */
35
+ server: Server;
36
+ /** Ferme le serveur (idempotent). */
37
+ close(): Promise<void>;
38
+ }
39
+ /**
40
+ * Démarre `target` sur `127.0.0.1:0` (port libre) et résout quand il écoute.
41
+ * `close()` ne ferme le serveur que si mjs-unit l'a lui-même créé à partir d'un
42
+ * handler (un `http.Server` fourni par l'appelant reste sous sa responsabilité).
43
+ */
44
+ export declare function startServer(target: ServerTarget): Promise<TestServer>;
45
+ /** Réponse normalisée d'une requête de test. */
46
+ export interface TestResponse {
47
+ /** Code de statut HTTP. */
48
+ status: number;
49
+ /** En-têtes de réponse. */
50
+ headers: IncomingHttpHeaders;
51
+ /** Corps brut (texte). */
52
+ text: string;
53
+ /** Corps parsé en JSON si le contenu est du JSON valide ; sinon le texte. */
54
+ body: any;
55
+ }
56
+ /** Options d'une requête. */
57
+ export interface RequestOptions {
58
+ /** Méthode HTTP (défaut : `GET`). */
59
+ method?: string;
60
+ /** En-têtes additionnels. */
61
+ headers?: Record<string, string>;
62
+ /**
63
+ * Corps de requête. Un objet est sérialisé en JSON avec
64
+ * `Content-Type: application/json` (sauf en-tête explicite). Une chaîne ou un
65
+ * `Buffer` est envoyé tel quel.
66
+ */
67
+ body?: unknown;
68
+ }
69
+ /** Client HTTP de test générique. */
70
+ export interface HttpTester {
71
+ request(path: string, opts?: RequestOptions): Promise<TestResponse>;
72
+ get(path: string, opts?: RequestOptions): Promise<TestResponse>;
73
+ post(path: string, body?: unknown, opts?: RequestOptions): Promise<TestResponse>;
74
+ put(path: string, body?: unknown, opts?: RequestOptions): Promise<TestResponse>;
75
+ del(path: string, opts?: RequestOptions): Promise<TestResponse>;
76
+ }
77
+ /** Effectue une requête unique vers `baseUrl + path`. */
78
+ export declare function fetchRequest(baseUrl: string, path: string, opts?: RequestOptions): Promise<TestResponse>;
79
+ /**
80
+ * Construit un client HTTP de test.
81
+ * - `target` = URL (`string`) : requêtes vers ce serveur déjà démarré ;
82
+ * - `target` = handler/serveur : un serveur éphémère est ouvert puis fermé à
83
+ * chaque requête (façon supertest, sans état partagé).
84
+ */
85
+ export declare function createHttpTester(target: ServerTarget | string): HttpTester;
86
+ /** Émetteur minimal compatible (EventEmitter, client Socket.io, ws…). */
87
+ export interface MinimalEmitter {
88
+ once?(event: string, listener: (...args: any[]) => void): unknown;
89
+ on(event: string, listener: (...args: any[]) => void): unknown;
90
+ off?(event: string, listener: (...args: any[]) => void): unknown;
91
+ removeListener?(event: string, listener: (...args: any[]) => void): unknown;
92
+ }
93
+ /** Options de `waitForEvent`. */
94
+ export interface WaitForEventOptions {
95
+ /** Délai maximal en millisecondes avant rejet (défaut : 4000). */
96
+ timeout?: number;
97
+ }
98
+ /**
99
+ * Résout avec la (première) charge utile du prochain événement `name`, ou
100
+ * rejette après `timeout`. Compatible avec tout émetteur exposant `on`
101
+ * (et idéalement `once`/`off`) : Socket.io, `ws`, `EventEmitter`…
102
+ */
103
+ export declare function waitForEvent<T = any>(emitter: MinimalEmitter, name: string, opts?: WaitForEventOptions): Promise<T>;
104
+ //# sourceMappingURL=web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"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"}
package/dist/web.js ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @mostajs/mjs-unit/web — extension de test web (zéro dépendance).
3
+ *
4
+ * Moteur générique pour tester des applications HTTP et événementielles sans
5
+ * tirer supertest, jest ou socket.io-client dans le framework lui-même :
6
+ *
7
+ * - `startServer(target)` : démarre un serveur HTTP éphémère (port 0) autour
8
+ * d'un `http.Server`, d'un `RequestListener` ou d'une app Express
9
+ * (`(req,res) => …`). Rend `{ url, port, close() }`. Utile quand le test a
10
+ * besoin d'un serveur durable (ex. connexion WebSocket/Socket.io).
11
+ * - `createHttpTester(target)` : client HTTP générique façon supertest.
12
+ * `target` peut être une URL de base (serveur déjà démarré) OU un
13
+ * handler/serveur (un serveur éphémère est alors ouvert puis fermé à CHAQUE
14
+ * requête). Méthodes `get/post/put/del/request` → `{ status, headers, body }`.
15
+ * - `waitForEvent(emitter, name, opts)` : attend un événement sur n'importe
16
+ * quel émetteur compatible (`EventEmitter`, client Socket.io, `ws`…) avec
17
+ * garde-fou de délai. Permet de tester le temps réel sans dépendance.
18
+ *
19
+ * S'appuie uniquement sur les modules natifs `node:http`/`node:net`. Aucune
20
+ * dépendance ajoutée : conforme à la philosophie zéro-dép de @mostajs/mjs-unit.
21
+ *
22
+ * @author Dr Hamid MADANI <drmdh@msn.com>
23
+ * @license AGPL-3.0-or-later
24
+ */
25
+ import http from "node:http";
26
+ function asServer(target) {
27
+ if (typeof target === "function") {
28
+ return { server: http.createServer(target), owned: true };
29
+ }
30
+ return { server: target, owned: false };
31
+ }
32
+ /**
33
+ * Démarre `target` sur `127.0.0.1:0` (port libre) et résout quand il écoute.
34
+ * `close()` ne ferme le serveur que si mjs-unit l'a lui-même créé à partir d'un
35
+ * handler (un `http.Server` fourni par l'appelant reste sous sa responsabilité).
36
+ */
37
+ export function startServer(target) {
38
+ const { server, owned } = asServer(target);
39
+ return new Promise((resolve, reject) => {
40
+ server.once("error", reject);
41
+ server.listen(0, "127.0.0.1", () => {
42
+ server.removeListener("error", reject);
43
+ const { port } = server.address();
44
+ resolve({
45
+ url: `http://127.0.0.1:${port}`,
46
+ port,
47
+ server,
48
+ close: () => new Promise((res) => {
49
+ if (!owned)
50
+ return res();
51
+ server.close(() => res());
52
+ }),
53
+ });
54
+ });
55
+ });
56
+ }
57
+ function encodeBody(body, headers) {
58
+ if (body === undefined || body === null)
59
+ return undefined;
60
+ if (typeof body === "string" || Buffer.isBuffer(body))
61
+ return body;
62
+ const hasType = Object.keys(headers).some((h) => h.toLowerCase() === "content-type");
63
+ if (!hasType)
64
+ headers["Content-Type"] = "application/json";
65
+ return JSON.stringify(body);
66
+ }
67
+ /** Effectue une requête unique vers `baseUrl + path`. */
68
+ export function fetchRequest(baseUrl, path, opts = {}) {
69
+ const url = new URL(path, baseUrl);
70
+ const headers = { ...(opts.headers ?? {}) };
71
+ const payload = encodeBody(opts.body, headers);
72
+ return new Promise((resolve, reject) => {
73
+ const req = http.request({
74
+ hostname: url.hostname,
75
+ port: url.port,
76
+ path: url.pathname + url.search,
77
+ method: opts.method ?? "GET",
78
+ headers,
79
+ }, (res) => {
80
+ const chunks = [];
81
+ res.on("data", (c) => chunks.push(c));
82
+ res.on("end", () => {
83
+ const text = Buffer.concat(chunks).toString("utf8");
84
+ let parsed = text;
85
+ const type = String(res.headers["content-type"] ?? "");
86
+ if (type.includes("application/json") && text.length) {
87
+ try {
88
+ parsed = JSON.parse(text);
89
+ }
90
+ catch {
91
+ /* garde le texte brut */
92
+ }
93
+ }
94
+ resolve({
95
+ status: res.statusCode ?? 0,
96
+ headers: res.headers,
97
+ text,
98
+ body: parsed,
99
+ });
100
+ });
101
+ });
102
+ req.on("error", reject);
103
+ if (payload !== undefined)
104
+ req.write(payload);
105
+ req.end();
106
+ });
107
+ }
108
+ /**
109
+ * Construit un client HTTP de test.
110
+ * - `target` = URL (`string`) : requêtes vers ce serveur déjà démarré ;
111
+ * - `target` = handler/serveur : un serveur éphémère est ouvert puis fermé à
112
+ * chaque requête (façon supertest, sans état partagé).
113
+ */
114
+ export function createHttpTester(target) {
115
+ async function once(path, opts) {
116
+ if (typeof target === "string") {
117
+ return fetchRequest(target, path, opts);
118
+ }
119
+ const srv = await startServer(target);
120
+ try {
121
+ return await fetchRequest(srv.url, path, opts);
122
+ }
123
+ finally {
124
+ await srv.close();
125
+ }
126
+ }
127
+ return {
128
+ request: (path, opts = {}) => once(path, opts),
129
+ get: (path, opts = {}) => once(path, { ...opts, method: "GET" }),
130
+ post: (path, body, opts = {}) => once(path, { ...opts, method: "POST", body }),
131
+ put: (path, body, opts = {}) => once(path, { ...opts, method: "PUT", body }),
132
+ del: (path, opts = {}) => once(path, { ...opts, method: "DELETE" }),
133
+ };
134
+ }
135
+ /**
136
+ * Résout avec la (première) charge utile du prochain événement `name`, ou
137
+ * rejette après `timeout`. Compatible avec tout émetteur exposant `on`
138
+ * (et idéalement `once`/`off`) : Socket.io, `ws`, `EventEmitter`…
139
+ */
140
+ export function waitForEvent(emitter, name, opts = {}) {
141
+ const timeout = opts.timeout ?? 4000;
142
+ return new Promise((resolve, reject) => {
143
+ const detach = () => {
144
+ if (typeof emitter.off === "function")
145
+ emitter.off(name, handler);
146
+ else if (typeof emitter.removeListener === "function")
147
+ emitter.removeListener(name, handler);
148
+ };
149
+ const handler = (...args) => {
150
+ clearTimeout(timer);
151
+ detach();
152
+ resolve(args.length <= 1 ? args[0] : args);
153
+ };
154
+ const timer = setTimeout(() => {
155
+ detach();
156
+ reject(new Error(`événement « ${name} » non reçu après ${timeout} ms`));
157
+ }, timeout);
158
+ if (typeof emitter.once === "function")
159
+ emitter.once(name, handler);
160
+ else
161
+ emitter.on(name, handler);
162
+ });
163
+ }
164
+ //# sourceMappingURL=web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"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"}
package/llms.txt ADDED
@@ -0,0 +1,67 @@
1
+ # @mostajs/mjs-unit — fiche LLM
2
+
3
+ > Micro-framework de test ZÉRO DÉPENDANCE pour l'écosystème @mostajs/* : API test/expect, runner CLI,
4
+ > et un resolver ESM qui exécute sous Node les dist à imports SANS extension (moduleResolution=bundler).
5
+
6
+ - Version: 0.2.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
7
+ - Chemin: mostajs/mosta-mjs-unit
8
+
9
+ ## RÔLE
10
+ Permettre à chaque module @mostajs/* d'avoir des tests reproductibles et versionnés sans tirer jest/vitest.
11
+ Résout aussi le point dur de l'écosystème : les `dist/` compilés en `moduleResolution: bundler` omettent
12
+ les extensions `.js` des imports relatifs → non exécutables par `node` seul. Le resolver de mjs-unit
13
+ réécrit `./x` → `./x.js` puis `./x/index.js`.
14
+
15
+ ## EXPORTS
16
+ - `.` (dist/index.js) : test, test.skip, describe, expect, ok, equal, deepEqual, throwsAsync,
17
+ doesNotThrowAsync, run, pending ; type TestCase, RunResult.
18
+ - `./web` (dist/web.js) : moteur de test web GÉNÉRIQUE, zéro dépendance (node:http/net) —
19
+ startServer, createHttpTester, fetchRequest, waitForEvent ; types TestServer, HttpTester,
20
+ TestResponse, RequestOptions, MinimalEmitter.
21
+ - `./register` (register.mjs) : enregistre le resolver via `node --import`.
22
+ - `./loader` (loader.mjs) : le hook `resolve` (pour usage avancé).
23
+ - bin `mjs-unit` (bin/cli.mjs) : runner — active le resolver, importe les fichiers, exécute.
24
+
25
+ ## API
26
+ - test(name, fn): void // fn sync ou async ; enregistre un cas
27
+ - test.skip(name, fn): void
28
+ - describe(group, body): void // préfixe les noms
29
+ - expect(v): { toBe(e), toEqual(e), toBeTruthy(), toBeFalsy(), toContain(sub) }
30
+ - ok(cond, msg?) · equal(a, e, msg?) · deepEqual(a, e, msg?)
31
+ - throwsAsync(fn, msg?) · doesNotThrowAsync(fn, msg?)
32
+ - run(label?): Promise<{passed, failed, skipped}> // exécute les cas en attente ; exitCode=1 si échec
33
+ - pending(): number
34
+
35
+ ## API — WEB (./web, générique, zéro dépendance)
36
+ - startServer(target): Promise<TestServer> // target = http.Server | RequestListener | app Express
37
+ → { url, port, server, close() } ; écoute sur 127.0.0.1:0 ; close() ne ferme que les serveurs créés par mjs-unit
38
+ - createHttpTester(target): HttpTester // target = URL string (serveur démarré) OU handler/serveur (éphémère/requête)
39
+ → request(path,opts?) / get / post(path,body?) / put / del → TestResponse { status, headers, text, body }
40
+ (body parsé JSON si content-type json ; objet en body → JSON + Content-Type auto)
41
+ - fetchRequest(baseUrl, path, opts?): Promise<TestResponse> // requête unique bas niveau
42
+ - waitForEvent(emitter, name, {timeout=4000}?): Promise<payload> // EventEmitter | socket.io-client | ws ;
43
+ résout 1er arg (ou tableau si plusieurs) ; rejette après timeout. NE PAS unref le timer (sortie node 13).
44
+
45
+ ## PATTERN
46
+ ```js
47
+ // test-scripts/x.test.mjs — NE PAS appeler run() (le runner le fait)
48
+ import { test, expect } from '@mostajs/mjs-unit'
49
+ import { f } from '../dist/lib/f.js' // imports sans extension du dist résolus par le loader
50
+ test('f', () => expect(f(2)).toBe(4))
51
+ ```
52
+ ```bash
53
+ # runner versionné : test-scripts/run-tests.sh
54
+ npm run build >/dev/null
55
+ node node_modules/@mostajs/mjs-unit/bin/cli.mjs test-scripts/*.test.mjs
56
+ ```
57
+
58
+ ## PIÈGES
59
+ - Importer la racine d'un module @mostajs qui ré-exporte des composants React (ex. @mostajs/audit → AuditPage)
60
+ charge react/lucide-react. En test, importer par SOUS-CHEMINS (dist/lib/*.js) pour éviter ces peers.
61
+ - Le runner exécute les cas en attente APRÈS import du fichier. Si le fichier appelle run() lui-même,
62
+ le runner ne ré-exécute pas (pending()===0). Choisir l'un ou l'autre.
63
+ - Le resolver n'agit que sur les specifiers RELATIFS sans extension ; les packages (bare) passent normalement.
64
+
65
+ ## CONVENTION (écosystème)
66
+ Tout test ET son runner (`run-tests.sh`) sont versionnés dans `test-scripts/` ; `package.json` `scripts.test`
67
+ pointe sur le runner. Jamais de test jetable.
package/loader.mjs ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @mostajs/mjs-unit — resolver ESM. Rend exécutables sous Node les imports relatifs SANS extension
3
+ * (convention `moduleResolution: bundler` des modules @mostajs/* dont le `dist/` omet le `.js`).
4
+ * Stratégie : pour un specifier relatif sans extension connue, essayer `+.js` puis `/index.js`.
5
+ * @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
6
+ */
7
+ const HAS_EXT = /\.(m?js|cjs|json|node|wasm)$/i;
8
+
9
+ export async function resolve(specifier, context, nextResolve) {
10
+ if (specifier.startsWith(".") && !HAS_EXT.test(specifier)) {
11
+ for (const cand of [specifier + ".js", specifier + "/index.js"]) {
12
+ try { return await nextResolve(cand, context); } catch { /* essai suivant */ }
13
+ }
14
+ }
15
+ return nextResolve(specifier, context);
16
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@mostajs/mjs-unit",
3
+ "version": "0.2.0",
4
+ "description": "Micro-framework de test pour l'écosystème @mostajs/* : zéro dépendance, API minimale (test/expect/run), runner CLI, et un resolver ESM qui exécute sous Node les dist à imports sans extension (convention moduleResolution=bundler).",
5
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
+ "license": "AGPL-3.0-or-later",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "bin": {
11
+ "mjs-unit": "bin/cli.mjs"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "default": "./dist/index.js"
18
+ },
19
+ "./web": {
20
+ "types": "./dist/web.d.ts",
21
+ "import": "./dist/web.js",
22
+ "default": "./dist/web.js"
23
+ },
24
+ "./register": {
25
+ "default": "./register.mjs"
26
+ },
27
+ "./loader": {
28
+ "default": "./loader.mjs"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "bin",
34
+ "loader.mjs",
35
+ "register.mjs",
36
+ "llms.txt",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "test": "bash test-scripts/run-tests.sh"
43
+ },
44
+ "devDependencies": {
45
+ "typescript": "^5.6.0",
46
+ "@types/node": "^22.0.0"
47
+ },
48
+ "keywords": [
49
+ "test",
50
+ "unit",
51
+ "esm",
52
+ "bundler",
53
+ "zero-dependency",
54
+ "mostajs"
55
+ ]
56
+ }
package/register.mjs ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @mostajs/mjs-unit — enregistre le resolver. Usage : `node --import @mostajs/mjs-unit/register tests.mjs`.
3
+ * @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
4
+ */
5
+ import { register } from "node:module";
6
+ register(new URL("./loader.mjs", import.meta.url));