# @mostajs/mjs-unit — fiche LLM

> Micro-framework de test ZÉRO DÉPENDANCE pour l'écosystème @mostajs/* : API test/expect, runner CLI,
> et un resolver ESM qui exécute sous Node les dist à imports SANS extension (moduleResolution=bundler).

- Version: 0.2.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
- Chemin: mostajs/mosta-mjs-unit

## RÔLE
Permettre à chaque module @mostajs/* d'avoir des tests reproductibles et versionnés sans tirer jest/vitest.
Résout aussi le point dur de l'écosystème : les `dist/` compilés en `moduleResolution: bundler` omettent
les extensions `.js` des imports relatifs → non exécutables par `node` seul. Le resolver de mjs-unit
réécrit `./x` → `./x.js` puis `./x/index.js`.

## EXPORTS
- `.` (dist/index.js) : test, test.skip, describe, expect, ok, equal, deepEqual, throwsAsync,
  doesNotThrowAsync, run, pending ; type TestCase, RunResult.
- `./web` (dist/web.js) : moteur de test web GÉNÉRIQUE, zéro dépendance (node:http/net) —
  startServer, createHttpTester, fetchRequest, waitForEvent ; types TestServer, HttpTester,
  TestResponse, RequestOptions, MinimalEmitter.
- `./register` (register.mjs) : enregistre le resolver via `node --import`.
- `./loader` (loader.mjs) : le hook `resolve` (pour usage avancé).
- bin `mjs-unit` (bin/cli.mjs) : runner — active le resolver, importe les fichiers, exécute.

## API
- test(name, fn): void  // fn sync ou async ; enregistre un cas
- test.skip(name, fn): void
- describe(group, body): void  // préfixe les noms
- expect(v): { toBe(e), toEqual(e), toBeTruthy(), toBeFalsy(), toContain(sub) }
- ok(cond, msg?) · equal(a, e, msg?) · deepEqual(a, e, msg?)
- throwsAsync(fn, msg?) · doesNotThrowAsync(fn, msg?)
- run(label?): Promise<{passed, failed, skipped}>  // exécute les cas en attente ; exitCode=1 si échec
- pending(): number

## API — WEB (./web, générique, zéro dépendance)
- startServer(target): Promise<TestServer>  // target = http.Server | RequestListener | app Express
    → { url, port, server, close() } ; écoute sur 127.0.0.1:0 ; close() ne ferme que les serveurs créés par mjs-unit
- createHttpTester(target): HttpTester  // target = URL string (serveur démarré) OU handler/serveur (éphémère/requête)
    → request(path,opts?) / get / post(path,body?) / put / del → TestResponse { status, headers, text, body }
      (body parsé JSON si content-type json ; objet en body → JSON + Content-Type auto)
- fetchRequest(baseUrl, path, opts?): Promise<TestResponse>  // requête unique bas niveau
- waitForEvent(emitter, name, {timeout=4000}?): Promise<payload>  // EventEmitter | socket.io-client | ws ;
    résout 1er arg (ou tableau si plusieurs) ; rejette après timeout. NE PAS unref le timer (sortie node 13).

## PATTERN
```js
// test-scripts/x.test.mjs — NE PAS appeler run() (le runner le fait)
import { test, expect } from '@mostajs/mjs-unit'
import { f } from '../dist/lib/f.js'   // imports sans extension du dist résolus par le loader
test('f', () => expect(f(2)).toBe(4))
```
```bash
# runner versionné : test-scripts/run-tests.sh
npm run build >/dev/null
node node_modules/@mostajs/mjs-unit/bin/cli.mjs test-scripts/*.test.mjs
```

## PIÈGES
- Importer la racine d'un module @mostajs qui ré-exporte des composants React (ex. @mostajs/audit → AuditPage)
  charge react/lucide-react. En test, importer par SOUS-CHEMINS (dist/lib/*.js) pour éviter ces peers.
- Le runner exécute les cas en attente APRÈS import du fichier. Si le fichier appelle run() lui-même,
  le runner ne ré-exécute pas (pending()===0). Choisir l'un ou l'autre.
- Le resolver n'agit que sur les specifiers RELATIFS sans extension ; les packages (bare) passent normalement.

## CONVENTION (écosystème)
Tout test ET son runner (`run-tests.sh`) sont versionnés dans `test-scripts/` ; `package.json` `scripts.test`
pointe sur le runner. Jamais de test jetable.
