feat: scaffold projet avec stubs et test runner navigateur
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Tests — Convertisseur Images</title>
|
||||
<style>
|
||||
body { font-family: monospace; padding: 20px; background: #f9f9f9; }
|
||||
.pass { color: green; margin: 2px 0; }
|
||||
.fail { color: red; font-weight: bold; margin: 2px 0; }
|
||||
h2 { margin-top: 20px; border-top: 1px solid #ddd; padding-top: 10px; }
|
||||
#summary { font-size: 1.2em; font-weight: bold; margin-top: 24px; padding: 12px;
|
||||
border-radius: 8px; display: inline-block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Tests unitaires</h1>
|
||||
<div id="results"></div>
|
||||
<div id="summary"></div>
|
||||
|
||||
<script type="module">
|
||||
let passed = 0, failed = 0;
|
||||
|
||||
function assert(description, condition) {
|
||||
const ok = Boolean(condition);
|
||||
ok ? passed++ : failed++;
|
||||
const el = document.createElement('div');
|
||||
el.className = ok ? 'pass' : 'fail';
|
||||
el.textContent = `${ok ? '✅' : '❌'} ${description}`;
|
||||
document.getElementById('results').appendChild(el);
|
||||
}
|
||||
|
||||
function section(title) {
|
||||
const el = document.createElement('h2');
|
||||
el.textContent = title;
|
||||
document.getElementById('results').appendChild(el);
|
||||
}
|
||||
|
||||
// Helper: construit un ImageData-like depuis un tableau 2D de [R,G,B]
|
||||
function makeImageData(grid) {
|
||||
const h = grid.length, w = grid[0].length;
|
||||
const data = new Uint8ClampedArray(w * h * 4);
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = (y * w + x) * 4;
|
||||
const [r, g, b] = grid[y][x];
|
||||
data[i] = r; data[i+1] = g; data[i+2] = b; data[i+3] = 255;
|
||||
}
|
||||
}
|
||||
return { data, width: w, height: h };
|
||||
}
|
||||
|
||||
// ── heicConverter ──────────────────────────────────────────────
|
||||
import { convertHeicToJpeg } from './js/heicConverter.js';
|
||||
|
||||
section('heicConverter');
|
||||
|
||||
try {
|
||||
await convertHeicToJpeg(null);
|
||||
assert('convertHeicToJpeg(null) lance TypeError', false);
|
||||
} catch (e) {
|
||||
assert('convertHeicToJpeg(null) lance TypeError', e instanceof TypeError);
|
||||
}
|
||||
|
||||
try {
|
||||
const pngFile = new File([new Uint8Array([137,80,78,71])], 'test.png', { type: 'image/png' });
|
||||
await convertHeicToJpeg(pngFile);
|
||||
assert('convertHeicToJpeg avec PNG lance TypeError', false);
|
||||
} catch (e) {
|
||||
assert('convertHeicToJpeg avec PNG lance TypeError', e instanceof TypeError);
|
||||
}
|
||||
|
||||
// ── bgRemover — chebyshevDistance ──────────────────────────────
|
||||
import { chebyshevDistance, createFloodFillMask, applyMaskToImageData } from './js/bgRemover.js';
|
||||
|
||||
section('bgRemover — chebyshevDistance');
|
||||
|
||||
assert('couleurs identiques → distance 0',
|
||||
chebyshevDistance([255,255,255], [255,255,255]) === 0);
|
||||
assert('blanc vs quasi-blanc (diff max=10) → distance 10',
|
||||
chebyshevDistance([255,255,255], [245,250,252]) === 10);
|
||||
assert('blanc vs gris 128 → distance 127',
|
||||
chebyshevDistance([255,255,255], [128,128,128]) === 127);
|
||||
assert('distance = max des écarts par canal',
|
||||
chebyshevDistance([200,100,50], [180,90,20]) === 30);
|
||||
|
||||
// ── bgRemover — createFloodFillMask ────────────────────────────
|
||||
section('bgRemover — createFloodFillMask');
|
||||
|
||||
const W = [255,255,255], R = [200,50,50], B = [50,50,200];
|
||||
|
||||
const img3x3 = makeImageData([
|
||||
[W, W, W],
|
||||
[W, R, W],
|
||||
[W, W, W],
|
||||
]);
|
||||
const mask3x3 = createFloodFillMask(img3x3, [255,255,255], 30);
|
||||
assert('3×3 : 8 pixels blancs de bordure masqués (somme=8)',
|
||||
Array.from(mask3x3).reduce((a,b) => a+b, 0) === 8);
|
||||
assert('3×3 : pixel rouge central NON masqué',
|
||||
mask3x3[1 * 3 + 1] === 0);
|
||||
|
||||
const img4x4 = makeImageData([
|
||||
[W, W, W, W],
|
||||
[W, B, B, W],
|
||||
[W, B, B, W],
|
||||
[W, W, W, W],
|
||||
]);
|
||||
const mask4x4 = createFloodFillMask(img4x4, [255,255,255], 30);
|
||||
assert('4×4 : 12 pixels blancs masqués',
|
||||
Array.from(mask4x4).reduce((a,b) => a+b, 0) === 12);
|
||||
assert('4×4 : 4 pixels bleus intérieurs non masqués',
|
||||
mask4x4[1*4+1] === 0 && mask4x4[1*4+2] === 0 &&
|
||||
mask4x4[2*4+1] === 0 && mask4x4[2*4+2] === 0);
|
||||
|
||||
// ── bgRemover — applyMaskToImageData ───────────────────────────
|
||||
section('bgRemover — applyMaskToImageData');
|
||||
|
||||
const imgApply = makeImageData([
|
||||
[[200,50,50], [50,50,200]],
|
||||
[[50,50,200], [50,50,200]],
|
||||
]);
|
||||
const maskApply = new Uint8Array([1, 0, 0, 0]);
|
||||
applyMaskToImageData(imgApply, maskApply, [255,255,255]);
|
||||
const d = imgApply.data;
|
||||
assert('pixel masqué → blanc pur',
|
||||
d[0] === 255 && d[1] === 255 && d[2] === 255);
|
||||
assert('pixel non masqué → inchangé (bleu)',
|
||||
d[4] === 50 && d[5] === 50 && d[6] === 200);
|
||||
|
||||
// ── imageProcessor — processBatch ──────────────────────────────
|
||||
import { processBatch } from './js/imageProcessor.js';
|
||||
|
||||
section('imageProcessor — processBatch');
|
||||
|
||||
const resultsA = [];
|
||||
await processBatch([1,2,3,4,5], async (item) => { resultsA.push(item * 2); }, 3);
|
||||
assert('processBatch traite les 5 éléments', resultsA.length === 5);
|
||||
assert('processBatch retourne les bons résultats',
|
||||
[2,4,6,8,10].every(v => resultsA.includes(v)));
|
||||
|
||||
let concurrent = 0, maxConcurrent = 0;
|
||||
await processBatch([1,2,3,4,5,6], async () => {
|
||||
concurrent++;
|
||||
maxConcurrent = Math.max(maxConcurrent, concurrent);
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
concurrent--;
|
||||
}, 3);
|
||||
assert('processBatch ne dépasse jamais 3 en simultané', maxConcurrent <= 3);
|
||||
|
||||
// ── zipExporter — downloadAllAsZip ─────────────────────────────
|
||||
import { downloadAllAsZip } from './js/zipExporter.js';
|
||||
|
||||
section('zipExporter — downloadAllAsZip');
|
||||
|
||||
let capturedHref = null;
|
||||
const origAppend = document.body.appendChild.bind(document.body);
|
||||
const origRemove = document.body.removeChild.bind(document.body);
|
||||
document.body.appendChild = (el) => {
|
||||
if (el.tagName === 'A') { capturedHref = el.href; el.click = () => {}; return el; }
|
||||
return origAppend(el);
|
||||
};
|
||||
document.body.removeChild = (el) => { try { origRemove(el); } catch {} };
|
||||
|
||||
await downloadAllAsZip([
|
||||
{ name: '12345.jpg', blob: new Blob(['data1'], { type: 'image/jpeg' }) },
|
||||
{ name: '67890.jpg', blob: new Blob(['data2'], { type: 'image/jpeg' }) },
|
||||
]);
|
||||
document.body.appendChild = origAppend;
|
||||
document.body.removeChild = origRemove;
|
||||
|
||||
assert('downloadAllAsZip génère une URL blob:',
|
||||
capturedHref !== null && capturedHref.startsWith('blob:'));
|
||||
|
||||
// ── Résumé ─────────────────────────────────────────────────────
|
||||
const summary = document.getElementById('summary');
|
||||
summary.textContent = `${passed} passé(s), ${failed} échoué(s)`;
|
||||
summary.style.background = failed === 0 ? '#dcfce7' : '#fee2e2';
|
||||
summary.style.color = failed === 0 ? '#166534' : '#991b1b';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user