test: ajout 22 scripts TC (TC-08 a TC-31) + tc-header.sh partage

This commit is contained in:
fabres
2026-08-04 21:08:43 +02:00
parent 3758d0ca85
commit 5db30ddeb3
23 changed files with 1839 additions and 0 deletions
+240
View File
@@ -0,0 +1,240 @@
#!/usr/bin/env bash
# ============================================================================
# TC-08 — Système de Jet V1 (RdDRoll)
# Usage: ./tc-08-roll-v1.sh [--navigate]
# Env: URL, PASS (admin password), PASS_USER (user password)
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-08 — Système de Jet V1 (RdDRoll) ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
# Setup: créer un acteur de test + items
# ============================================================================
SETUP=$(jse_get_async "
const ex = game.actors.getName('TC-08 Test');
if (ex) await ex.delete();
const [actor] = await Actor.createDocuments([{
type: 'personnage', name: 'TC-08 Test',
system: {
carac: { force: {value: 12}, intellect: {value: 10}, dexterite: {value: 11} },
reve: {reve: {value: 12}, seui: {value: 12} },
sante: { vie: {value: 12, max: 12}, endurance: {value: 20, max: 20} }
}
}]);
if (!actor) return {error: 'create failed'};
const [comp] = await actor.createEmbeddedDocuments('Item', [{
type: 'competence', name: 'TC-08 Escrime',
system: { categorie: 'melee', niveau: 5, xp: 0, base: 0, carac: 'force' }
}]);
const [compN] = await actor.createEmbeddedDocuments('Item', [{
type: 'competence', name: 'TC-08 Natation',
system: { categorie: 'physique', niveau: 3, xp: 0, base: 0, carac: 'force' }
}]);
const [arme] = await actor.createEmbeddedDocuments('Item', [{
type: 'arme', name: 'TC-08 Epee',
system: { categorie: 'cac', degats: '3', competence: 'TC-08 Escrime', competence2: '', encombrement: 2, mains: 1 }
}]);
const [sort] = await actor.createEmbeddedDocuments('Item', [{
type: 'sort', name: 'TC-08 Lueur',
system: { niveau: 4, voie: 'Oniros', draconic: 'Oniros', coutreve: 2, description: '' }
}]);
return { actorId: actor.id, compId: comp?.id, armeId: arme?.id, sortId: sort?.id };
")
ERR=$(eval_ok "$SETUP" error)
[ -z "$ERR" ] || [ "$ERR" = "None" ] || die "Setup: $ERR"
AID=$(eval_ok "$SETUP" actorId)
COMP_ID=$(eval_ok "$SETUP" compId)
ARME_ID=$(eval_ok "$SETUP" armeId)
SORT_ID=$(eval_ok "$SETUP" sortId)
# Helper: close all rdd-roll-dialogs
close_roll_dialogs() {
jse_get_async "
const apps = Object.values(ui.windows).filter(w => w.element?.classList?.contains('rdd-roll-dialog'));
for (const a of apps) { a.rollData = a.rollData || {}; a.rollData.canClose = true; await a.close(); }
return 'closed';
" >/dev/null 2>&1
}
# ============================================================================
echo "--- TC-08-001: Jet Compétence — dialogue s'ouvre ---"
# ============================================================================
R=$(jse_get_async "
try {
const a = game.actors.get('$AID');
if (!a) return {error: 'no actor'};
await a.rollCompetence('$COMP_ID');
await new Promise(r => setTimeout(r, 2000));
const dlg = document.querySelector('.rdd-roll-dialog');
const ok = !!dlg;
return { opened: ok };
} catch(e) { return {error: e?.message || String(e)}; }
")
OPENED=$(eval_ok "$R" opened)
ERR=$(eval_ok "$R" error)
[ "$OPENED" = "True" ] && pass "TC-08-001: dialog RdDRoll ouvert" || fail "TC-08-001: dialog KO ($ERR)"
close_roll_dialogs
# ============================================================================
echo "--- TC-08-002: Jet Compétence — modificateurs ---"
# ============================================================================
R=$(jse_get_async "
try {
const a = game.actors.get('$AID');
if (!a) return {error: 'no actor'};
const { RdDRoll } = await import('/systems/foundryvtt-reve-de-dragon/module/rdd-roll.js');
const comp = a.items.get('$COMP_ID');
const rollData = {
carac: { force: a.system.carac.force },
selectedCarac: a.system.carac.force,
competence: comp,
diffLibre: 10,
ajustementForce: 5,
forceDiceResult: -1
};
const content = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/dialog-roll-competence.hbs', rollData);
const dialog = new RdDRoll(a, rollData, content, {classes:['rdd-roll-dialog'], width:650, height:'fit-content'}, {
name: 'comp-test', label: 'Test Comp',
callbacks: [{action: r => { r.rolled = {isSuccess: true, code: 'normale'}; }}]
});
await dialog.render(true);
await new Promise(r => setTimeout(r, 800));
const el = dialog.element;
const hasDiff = !!el?.querySelector('[name=diffLibre]');
const hasCarac = !!el?.querySelector('[name=carac]');
dialog.rollData.canClose = true;
await dialog.close();
return { opened: !!el, hasDiff, hasCarac };
} catch(e) { return {error: e?.message || String(e)}; }
")
HASDIFF=$(eval_ok "$R" hasDiff)
[ "$HASDIFF" = "True" ] && pass "TC-08-002: champ difficulté visible" || fail "TC-08-002: KO ($HASDIFF)"
close_roll_dialogs
# ============================================================================
echo "--- TC-08-004: Jet Attaque ---"
# ============================================================================
R=$(jse_get_async "
try {
const a = game.actors.get('$AID');
if (!a) return {error: 'no actor'};
const arme = a.items.get('$ARME_ID');
if (!arme) return {error: 'no arme'};
await a.rollArme(arme, 'competence');
await new Promise(r => setTimeout(r, 2000));
const dlg = document.querySelector('.rdd-roll-dialog');
const ok = !!dlg;
return { opened: ok };
} catch(e) { return {error: e?.message || String(e)}; }
")
OPENED=$(eval_ok "$R" opened)
ERR=$(eval_ok "$R" error)
[ "$OPENED" = "True" ] && pass "TC-08-004: dialog attaque ouvert" || fail "TC-08-004: KO ($ERR)"
close_roll_dialogs
# ============================================================================
echo "--- TC-08-007: Jet Sort ---"
# ============================================================================
R=$(jse_get_async "
try {
const a = game.actors.get('$AID');
if (!a) return {error: 'no actor'};
const sort = a.items.get('$SORT_ID');
if (!sort) return {error: 'no sort'};
if (typeof a.rollSort === 'function') await a.rollSort(sort.id);
else if (typeof a.rollSortReserve === 'function') await a.rollSortReserve(sort.id);
await new Promise(r => setTimeout(r, 2000));
const dlg = document.querySelector('.rdd-roll-dialog');
const ok = !!dlg;
return { opened: ok };
} catch(e) { return {error: e?.message || String(e)}; }
")
OPENED=$(eval_ok "$R" opened)
ERR=$(eval_ok "$R" error)
[ "$OPENED" = "True" ] && pass "TC-08-007: dialog sort ouvert" || fail "TC-08-007: KO ($ERR)"
close_roll_dialogs
# ============================================================================
echo "--- TC-08-009: Jet Éthylisme ---"
# ============================================================================
R=$(jse_get_async "
try {
const a = game.actors.get('$AID');
if (!a) return {error: 'no actor'};
await a.jetEthylisme();
await new Promise(r => setTimeout(r, 2000));
// jetEthylisme opens RdDRollDialogEthylisme, not RdDRoll
const dlg = document.querySelector('.rdd-roll-ethylisme, .dialog-ethylisme, [class*=ethylisme]');
const ok = !!dlg;
// Also check any new dialog
const allDialogs = document.querySelectorAll('.application.dialog');
return { opened: ok, dialogCount: allDialogs.length };
} catch(e) { return {error: e?.message || String(e)}; }
")
OPENED=$(eval_ok "$R" opened)
ERR=$(eval_ok "$R" error)
[ "$OPENED" = "True" ] && pass "TC-08-009: dialog éthylisme ouvert" || fail "TC-08-009: KO ($ERR)"
close_roll_dialogs
# ============================================================================
echo "--- TC-08-011: Message de résultat chat ---"
# ============================================================================
R=$(jse_get_async "
try {
const a = game.actors.get('$AID');
if (!a) return {error: 'no actor'};
const { RdDRoll } = await import('/systems/foundryvtt-reve-de-dragon/module/rdd-roll.js');
const comp = a.items.get('$COMP_ID');
const rollData = {
carac: { force: a.system.carac.force },
selectedCarac: a.system.carac.force,
competence: comp,
diffLibre: 0,
forceDiceResult: 50
};
const content = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/dialog-roll-competence.hbs', rollData);
const dialog = new RdDRoll(a, rollData, content, {classes:['rdd-roll-dialog'], width:650, height:'fit-content'}, {
name: 'comp-res', label: 'Test Result',
callbacks: [{action: r => { r.rolled = {isSuccess: true, code: 'particuliere'}; }}]
});
await dialog.render(true);
await new Promise(r => setTimeout(r, 600));
const btn = dialog.element?.querySelector('button[data-action=onAction]');
if (btn) btn.click();
await new Promise(r => setTimeout(r, 2000));
const msgs = game.messages.contents.slice(-2);
const found = msgs.some(m => (m.content || '').length > 50);
return { chatHasResult: found, msgCount: msgs.length };
} catch(e) { return {error: e?.message || String(e)}; }
")
CHATOK=$(eval_ok "$R" chatHasResult)
ERR=$(eval_ok "$R" error)
[ "$CHATOK" = "True" ] && pass "TC-08-011: message chat résultat roll" || fail "TC-08-011: KO ($ERR)"
close_roll_dialogs
# ============================================================================
echo ""
echo "--- Cleanup ---"
CLEANUP=$(jse_get_async "
const a = game.actors.getName('TC-08 Test');
if (a) { await a.delete(); return 'deleted'; }
return 'none';
" | tr -d '"\n')
[ "$CLEANUP" = "deleted" ] && pass "Cleanup: actor deleted" || fail "Cleanup: $CLEANUP"
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# ============================================================================
# TC-09 — Système de Jet V2 (Roll Dialog)
# Usage: ./tc-09-roll-v2.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-09 — Système de Jet V2 (Roll Dialog) ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# Setup
SETUP=$(jse_get_async "
const ex = game.actors.getName('TC-09 Test');
if (ex) await ex.delete();
const [actor] = await Actor.createDocuments([{
type: 'personnage', name: 'TC-09 Test',
system: { carac: { force: {value: 12} }, sante: { vie: {value: 12, max: 12}, endurance: {value: 20, max: 20} } }
}]);
const [comp] = await actor.createEmbeddedDocuments('Item', [{
type: 'competence', name: 'TC-09 Comp',
system: { categorie: 'physique', niveau: 5, xp: 0, base: 0, carac: 'force' }
}]);
return { actorId: actor.id, compId: comp?.id };
")
AID=$(eval_ok "$SETUP" actorId)
# ============================================================================
echo "--- TC-09-001: Toggle V2 activé ---"
# ============================================================================
R=$(jse_get_async "
try {
const SETTING = 'rdd-advanced-roll-dialog-v2';
const before = game.settings.get('foundryvtt-reve-de-dragon', SETTING);
await game.settings.set('foundryvtt-reve-de-dragon', SETTING, true);
const after = game.settings.get('foundryvtt-reve-de-dragon', SETTING);
return { before, after, ok: after === true };
} catch(e) { return {error: e.message}; }
")
OK=$(eval_ok "$R" ok)
ERR=$(eval_ok "$R" error)
[ "$OK" = "True" ] && pass "TC-09-001: ROLL_DIALOG_V2 activé" || fail "TC-09-001: KO ($ERR)"
# ============================================================================
echo "--- TC-09-002: Roll V2 — module chargé ---"
# ============================================================================
R=$(jse_get_async "
try {
const mod = await import('/systems/foundryvtt-reve-de-dragon/module/roll/roll-dialog.mjs');
const RollDialog = mod.default ?? mod.RollDialog;
return { ok: !!RollDialog, hasCreate: typeof RollDialog?.create === 'function' };
} catch(e) { return {error: e.message}; }
")
OK=$(eval_ok "$R" ok)
ERR=$(eval_ok "$R" error)
[ "$OK" = "True" ] && pass "TC-09-002: RollDialog V2 module chargé" || fail "TC-09-002: KO ($ERR)"
# ============================================================================
echo "--- TC-09-005: Toggle V2 désactivé ---"
# ============================================================================
R=$(jse_get_async "
try {
const SETTING = 'rdd-advanced-roll-dialog-v2';
await game.settings.set('foundryvtt-reve-de-dragon', SETTING, false);
const after = game.settings.get('foundryvtt-reve-de-dragon', SETTING);
return { after, ok: after === false };
} catch(e) { return {error: e.message}; }
")
OK=$(eval_ok "$R" ok)
ERR=$(eval_ok "$R" error)
[ "$OK" = "True" ] && pass "TC-09-005: ROLL_DIALOG_V2 désactivé" || fail "TC-09-005: KO ($ERR)"
# Cleanup
jse_get_async "
const a = game.actors.getName('TC-09 Test');
if (a) await a.delete();
return 'ok';
" >/dev/null 2>&1
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# ============================================================================
# TC-10 — Dés Custom
# Usage: ./tc-10-des-custom.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-10 — Dés Custom ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-10-001: Dé TMR (d8) ---"
# ============================================================================
R=$(jse_get_async "
try {
const dice = game.system.rdd.Dice;
if (!dice) return {error: 'no Dice'};
const roll = await dice.roll('1dt');
const total = roll.total;
const valid = total >= 1 && total <= 8;
return { total, valid, formula: roll.formula };
} catch(e) { return {error: e?.message || String(e)}; }
")
TOTAL=$(eval_ok "$R" total)
VALID=$(eval_ok "$R" valid)
ERR=$(eval_ok "$R" error)
[ "$VALID" = "True" ] && pass "TC-10-001: dé TMR = $TOTAL (1-8)" || fail "TC-10-001: KO ($ERR)"
# ============================================================================
echo "--- TC-10-002: Dé Draconique (d8 explosif) ---"
# ============================================================================
R=$(jse_get_async "
try {
const dice = game.system.rdd.Dice;
const roll = await dice.roll('1dd');
const total = roll.total;
// draconic die can explode on 7, total can be 0 on failure
const valid = total >= 0;
return { total, valid, formula: roll.formula };
} catch(e) { return {error: e?.message || String(e)}; }
")
TOTAL=$(eval_ok "$R" total)
VALID=$(eval_ok "$R" valid)
ERR=$(eval_ok "$R" error)
[ "$VALID" = "True" ] && pass "TC-10-002: dé draconique = $TOTAL" || fail "TC-10-002: KO ($ERR)"
# ============================================================================
echo "--- TC-10-003: Dé Heure (d12) ---"
# ============================================================================
R=$(jse_get_async "
try {
const dice = game.system.rdd.Dice;
if (typeof dice.rollHeure !== 'function') return {error: 'no rollHeure'};
const result = await dice.rollHeure();
// rollHeure returns a Number directly, not a Roll object
const total = typeof result === 'number' ? result : result?.total;
const valid = total >= 0 && total <= 11;
return { total, valid, type: typeof result };
} catch(e) { return {error: e?.message || String(e)}; }
")
TOTAL=$(eval_ok "$R" total)
VALID=$(eval_ok "$R" valid)
ERR=$(eval_ok "$R" error)
[ "$VALID" = "True" ] && pass "TC-10-003: dé heure = $TOTAL (0-11)" || fail "TC-10-003: KO ($ERR)"
# ============================================================================
echo "--- TC-10-005: Résolution d100 ---"
# ============================================================================
R=$(jse_get_async "
try {
const { RdDResolutionTable } = await import('/systems/foundryvtt-reve-de-dragon/module/rdd-resolution-table.js');
const result = await RdDResolutionTable.rollData({carac: 12, level: 0, forceDiceResult: 50});
const code = result?.rolled?.code;
const isSuccess = result?.rolled?.isSuccess;
return { code, isSuccess, hasRolled: !!result?.rolled };
} catch(e) { return {error: e?.message || String(e)}; }
")
CODE=$(eval_ok "$R" code)
HASROLLED=$(eval_ok "$R" hasRolled)
ERR=$(eval_ok "$R" error)
[ "$HASROLLED" = "True" ] && pass "TC-10-005: résolution d100 code=$CODE" || fail "TC-10-005: KO ($ERR)"
# ============================================================================
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# ============================================================================
# TC-11 — Combat
# Usage: ./tc-11-combat.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-11 — Combat ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# Setup
SETUP=$(jse_get_async "
const ex = game.actors.getName('TC-11 Test');
if (ex) await ex.delete();
const [actor] = await Actor.createDocuments([{
type: 'personnage', name: 'TC-11 Test',
system: { carac: { force: {value: 12} }, sante: { vie: {value: 12, max: 12}, endurance: {value: 20, max: 20} } }
}]);
const [arme] = await actor.createEmbeddedDocuments('Item', [{
type: 'arme', name: 'TC-11 Epee',
system: { categorie: 'cac', degats: '3', competence: 'Mêlée', competence2: '', encombrement: 2, mains: 1 }
}]);
return { actorId: actor.id, armeId: arme?.id };
")
AID=$(eval_ok "$SETUP" actorId)
# ============================================================================
echo "--- TC-11-001: Création combat ---"
# ============================================================================
R=$(jse_get_async "
try {
const { RdDCombat } = await import('/systems/foundryvtt-reve-de-dragon/module/combat/rdd-combat.js');
return { hasCombat: !!RdDCombat, hasCreate: typeof RdDCombat.create === 'function' };
} catch(e) { return {error: e.message}; }
")
HASC=$(eval_ok "$R" hasCombat)
ERR=$(eval_ok "$R" error)
[ "$HASC" = "True" ] && pass "TC-11-001: RdDCombat chargé" || fail "TC-11-001: KO ($ERR)"
# ============================================================================
echo "--- TC-11-002: Initiative — phases ---"
# ============================================================================
R=$(jse_get_async "
try {
const { RdDInitiative } = await import('/systems/foundryvtt-reve-de-dragon/module/combat/initiative.mjs');
return { hasInit: !!RdDInitiative, hasRoll: typeof RdDInitiative?.roll === 'function' };
} catch(e) { return {error: e.message}; }
")
HASINIT=$(eval_ok "$R" hasInit)
ERR=$(eval_ok "$R" error)
[ "$HASINIT" = "True" ] && pass "TC-11-002: RdDInitiative chargé" || fail "TC-11-002: KO ($ERR)"
# Cleanup
jse_get_async "const a = game.actors.getName('TC-11 Test'); if (a) await a.delete(); return 'ok';" >/dev/null 2>&1
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# ============================================================================
# TC-12 — Calendrier / Temps
# Usage: ./tc-12-calendrier.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-12 — Calendrier / Temps ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-12-001: Calendrier visible ---"
# ============================================================================
R=$(jse_get_async "
try {
const cal = game.system.rdd?.calendrier;
if (!cal) return {error: 'no calendrier'};
const el = document.querySelector('.calendar, #rdd-calendar, [class*=calendar]');
const visible = el !== null && el.offsetParent !== null;
return { exists: !!cal, visible, tag: el?.tagName };
} catch(e) { return {error: e?.message || String(e)}; }
")
EXISTS=$(eval_ok "$R" exists)
ERR=$(eval_ok "$R" error)
[ "$EXISTS" = "True" ] && pass "TC-12-001: calendrier existe" || fail "TC-12-001: KO ($ERR)"
# ============================================================================
echo "--- TC-12-002: Heure Draconique ---"
# ============================================================================
R=$(jse_get_async "
try {
const cal = game.system.rdd?.calendrier;
if (!cal) return {error: 'no calendrier'};
const timestamp = cal.timestamp || cal.getTimestamp?.();
const heure = cal.dateCourante?.()?.getHeure?.() ?? cal.getCurrentHeure?.();
const date = cal.dateCourante?.();
return { heure, hasDate: !!date, timestamp };
} catch(e) { return {error: e?.message || String(e)}; }
")
HEURE=$(eval_ok "$R" heure)
HASDATE=$(eval_ok "$R" hasDate)
ERR=$(eval_ok "$R" error)
[ "$HASDATE" = "True" ] && pass "TC-12-002: heure draconique = $HEURE" || fail "TC-12-002: KO ($ERR)"
# ============================================================================
echo "--- TC-12-003: Avancement du temps ---"
# ============================================================================
R=$(jse_get_async "
try {
const cal = game.system.rdd?.calendrier;
if (!cal) return {error: 'no calendrier'};
const ts = cal.timestamp;
const before = ts.indexDate + ':' + ts.indexMinute;
const newTs = ts.addJours(1);
if (cal.setNewTimestamp) await cal.setNewTimestamp(newTs);
else if (cal.incrementerJour) await cal.incrementerJour();
await new Promise(r => setTimeout(r, 500));
const after = cal.timestamp.indexDate + ':' + cal.timestamp.indexMinute;
const changed = before !== after;
return { before, after, changed };
} catch(e) { return {error: e?.message || String(e)}; }
")
CHANGED=$(eval_ok "$R" changed)
ERR=$(eval_ok "$R" error)
[ "$CHANGED" = "True" ] && pass "TC-12-003: temps avance" || fail "TC-12-003: KO ($ERR)"
# ============================================================================
echo "--- TC-12-004: Nombres Astraux ---"
# ============================================================================
R=$(jse_get_async "
try {
const cal = game.system.rdd?.calendrier;
if (!cal) return {error: 'no calendrier'};
const na = cal.getNombresAstraux?.() ?? cal.nombresAstraux;
return { hasNA: !!na, na: typeof na === 'object' ? JSON.stringify(na).substring(0, 200) : na };
} catch(e) { return {error: e?.message || String(e)}; }
")
HASNA=$(eval_ok "$R" hasNA)
ERR=$(eval_ok "$R" error)
[ "$HASNA" = "True" ] && pass "TC-12-004: nombres astraux disponibles" || fail "TC-12-004: KO ($ERR)"
# ============================================================================
echo "--- TC-12-007: Chronologie ---"
# ============================================================================
R=$(jse_get_async "
try {
const { DialogChronologie } = await import('/systems/foundryvtt-reve-de-dragon/module/dialog-chronologie.js');
if (!DialogChronologie) return {error: 'no DialogChronologie'};
await DialogChronologie.create();
await new Promise(r => setTimeout(r, 2000));
const el = document.querySelector('.dialog-chronologie, [class*=chronolog]');
const ok = !!el;
// Close it
const apps = Object.values(ui.windows).filter(w => w.constructor?.name?.includes('Chronolog'));
for (const a of apps) await a.close();
if (!apps.length && el) el.remove();
return { opened: ok };
} catch(e) { return {error: e?.message || String(e)}; }
")
OPENED=$(eval_ok "$R" opened)
ERR=$(eval_ok "$R" error)
[ "$OPENED" = "True" ] && pass "TC-12-007: chronologie ouverte" || fail "TC-12-007: KO ($ERR)"
# ============================================================================
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# ============================================================================
# TC-13 — Sommeil / Repos
# Usage: ./tc-13-sommeil.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-13 — Sommeil / Repos ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# Setup
SETUP=$(jse_get_async "
const ex = game.actors.getName('TC-13 Test');
if (ex) await ex.delete();
const [actor] = await Actor.createDocuments([{
type: 'personnage', name: 'TC-13 Test',
system: { sante: { vie: {value: 12, max: 12}, endurance: {value: 20, max: 20} } }
}]);
return { actorId: actor.id };
")
AID=$(eval_ok "$SETUP" actorId)
# ============================================================================
echo "--- TC-13-001: Repos (ancien) /sommeil ---"
# ============================================================================
R=$(jse_get_async "
try {
game.system.rdd.commands.onChatMessage('/sommeil', {content: '/sommeil', speaker: {alias: 'TC-13'}});
await new Promise(r => setTimeout(r, 2000));
const el = document.querySelector('[class*=chateau-dormant], [class*=sommeil], .dialog-chateau-dormant');
return { opened: !!el };
} catch(e) { return {error: e.message}; }
")
OPENED=$(eval_ok "$R" opened)
ERR=$(eval_ok "$R" error)
[ "$OPENED" = "True" ] && pass "TC-13-001: dialog sommeil ouvert" || fail "TC-13-001: KO ($ERR)"
jse_get_async "
const apps = Object.values(ui.windows).filter(w => w.constructor?.name?.includes('Chateau') || w.constructor?.name?.includes('Dormant') || w.constructor?.name?.includes('Sommeil'));
for (const a of apps) await a.close();
return 'closed';
" >/dev/null 2>&1
# ============================================================================
echo "--- TC-13-005: Distribution Stress GM /stress ---"
# ============================================================================
R=$(jse_get_async "
try {
game.system.rdd.commands.onChatMessage('/stress', {content: '/stress', speaker: {alias: 'TC-13'}});
await new Promise(r => setTimeout(r, 2000));
const el = document.querySelector('[class*=dialog-stress], [class*=stress]');
return { opened: !!el };
} catch(e) { return {error: e.message}; }
")
OPENED=$(eval_ok "$R" opened)
ERR=$(eval_ok "$R" error)
[ "$OPENED" = "True" ] && pass "TC-13-005: dialog stress ouvert" || fail "TC-13-005: KO ($ERR)"
jse_get_async "
const apps = Object.values(ui.windows).filter(w => w.constructor?.name?.includes('Stress'));
for (const a of apps) await a.close();
return 'closed';
" >/dev/null 2>&1
# Cleanup
jse_get_async "const a = game.actors.getName('TC-13 Test'); if (a) await a.delete(); return 'ok';" >/dev/null 2>&1
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# ============================================================================
# TC-14 — Voyage
# Usage: ./tc-14-voyage.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-14 — Voyage ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# Setup
SETUP=$(jse_get_async "
const ex = game.actors.getName('TC-14 Test');
if (ex) await ex.delete();
const [actor] = await Actor.createDocuments([{type:'personnage', name:'TC-14 Test'}]);
return { actorId: actor.id };
")
AID=$(eval_ok "$SETUP" actorId)
# ============================================================================
echo "--- TC-14-001: Dialogue Voyage ---"
# ============================================================================
R=$(jse_get_async "
try {
game.system.rdd.commands.onChatMessage('/voyage', {content: '/voyage', speaker: {alias: 'TC-14'}});
await new Promise(r => setTimeout(r, 2000));
const el = document.querySelector('[class*=fatigue-voyage], [class*=voyage], .dialog-fatigue-voyage');
return { opened: !!el };
} catch(e) { return {error: e.message}; }
")
OPENED=$(eval_ok "$R" opened)
ERR=$(eval_ok "$R" error)
[ "$OPENED" = "True" ] && pass "TC-14-001: dialog voyage ouvert" || fail "TC-14-001: KO ($ERR)"
# Close
jse_get_async "
const apps = Object.values(ui.windows).filter(w => w.constructor?.name?.includes('Voyage') || w.constructor?.name?.includes('Fatigue'));
for (const a of apps) await a.close();
return 'closed';
" >/dev/null 2>&1
# Cleanup
jse_get_async "const a = game.actors.getName('TC-14 Test'); if (a) await a.delete(); return 'ok';" >/dev/null 2>&1
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env bash
# ============================================================================
# TC-15 — TMR (Terres Médianes du Rêve)
# Usage: ./tc-15-tmr.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-15 — TMR (Terres Médianes du Rêve) ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# Setup: créer un acteur HR (haut rêvant)
SETUP=$(jse_get_async "
const ex = game.actors.getName('TC-15 Test');
if (ex) await ex.delete();
const [actor] = await Actor.createDocuments([{
type: 'personnage', name: 'TC-15 Test',
system: {
carac: { force: {value: 12}, reve: {value: 12} },
reve: { reve: {value: 12}, tmrpos: {coord: 'A1'} },
sante: { vie: {value: 12, max: 12}, endurance: {value: 20, max: 20} }
}
}]);
return { actorId: actor.id };
")
AID=$(eval_ok "$SETUP" actorId)
# ============================================================================
echo "--- TC-15-002: Génération rencontre TMR ---"
# ============================================================================
R=$(jse_get_async "
try {
const a = game.actors.get('$AID');
if (!a) return {error: 'no actor'};
// Check TMRRencontres exists
const { TMRRencontres } = await import('/systems/foundryvtt-reve-de-dragon/module/tmr-rencontres.js');
const hasRencontres = !!TMRRencontres;
return { hasRencontres };
} catch(e) { return {error: e?.message || String(e)}; }
")
HASREN=$(eval_ok "$R" hasRencontres)
ERR=$(eval_ok "$R" error)
[ "$HASREN" = "True" ] && pass "TC-15-002: TMRRencontres module chargé" || fail "TC-15-002: KO ($ERR)"
# ============================================================================
echo "--- TC-15-003: Recherche TMR ---"
# ============================================================================
R=$(jse_get_async "
try {
const { TMRUtility } = await import('/systems/foundryvtt-reve-de-dragon/module/tmr-utility.js');
const tmr = TMRUtility.getTMR('A1');
return { found: !!tmr, coord: tmr?.coord, type: tmr?.type, label: tmr?.label };
} catch(e) { return {error: e?.message || String(e)}; }
")
FOUND=$(eval_ok "$R" found)
COORD=$(eval_ok "$R" coord)
ERR=$(eval_ok "$R" error)
[ "$FOUND" = "True" ] && pass "TC-15-003: TMR A1 = $COORD" || fail "TC-15-003: KO ($ERR)"
# ============================================================================
echo "--- TC-15-004: TMR Aléatoire ---"
# ============================================================================
R=$(jse_get_async "
try {
const { TMRUtility } = await import('/systems/foundryvtt-reve-de-dragon/module/tmr-utility.js');
const tmr = TMRUtility.getTMRAleatoire?.() ?? TMRUtility.randomTMR?.();
return { found: !!tmr, coord: tmr?.coord, type: tmr?.type };
} catch(e) { return {error: e?.message || String(e)}; }
")
FOUND=$(eval_ok "$R" found)
COORD=$(eval_ok "$R" coord)
ERR=$(eval_ok "$R" error)
[ "$FOUND" = "True" ] && pass "TC-15-004: TMR aléatoire = $COORD" || fail "TC-15-004: KO ($ERR)"
# ============================================================================
echo "--- TC-15-006: Coordonnées TMR acteur ---"
# ============================================================================
R=$(jse_get_async "
try {
const a = game.actors.get('$AID');
if (!a) return {error: 'no actor'};
await a.update({'system.reve.tmrpos.coord': 'M5'});
await new Promise(r => setTimeout(r, 500));
const coord = a.system.reve?.tmrpos?.coord;
return { coord, ok: coord === 'M5' };
} catch(e) { return {error: e?.message || String(e)}; }
")
COORD=$(eval_ok "$R" coord)
OK=$(eval_ok "$R" ok)
ERR=$(eval_ok "$R" error)
[ "$OK" = "True" ] && pass "TC-15-006: coord TMR acteur = $COORD" || fail "TC-15-006: KO ($ERR)"
# ============================================================================
echo "--- TC-15-007: Réinsertion aléatoire ---"
# ============================================================================
R=$(jse_get_async "
try {
const a = game.actors.get('$AID');
if (!a) return {error: 'no actor'};
if (typeof a.reinsertionAleatoire !== 'function') return {error: 'no reinsertionAleatoire'};
await a.reinsertionAleatoire();
await new Promise(r => setTimeout(r, 500));
const coord = a.system.reve?.tmrpos?.coord;
return { newCoord: coord, changed: coord !== 'M5' };
} catch(e) { return {error: e?.message || String(e)}; }
")
NEWC=$(eval_ok "$R" newCoord)
CHANGED=$(eval_ok "$R" changed)
ERR=$(eval_ok "$R" error)
[ "$CHANGED" = "True" ] && pass "TC-15-007: réinsertion = $NEWC" || fail "TC-15-007: KO ($ERR)"
# ============================================================================
echo ""
echo "--- Cleanup ---"
CLEANUP=$(jse_get_async "
const a = game.actors.getName('TC-15 Test');
if (a) { await a.delete(); return 'deleted'; }
return 'none';
" | tr -d '"\n')
[ "$CLEANUP" = "deleted" ] && pass "Cleanup: actor deleted" || fail "Cleanup: $CLEANUP"
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# ============================================================================
# TC-16 — Commandes Chat
# Usage: ./tc-16-commandes-chat.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-16 — Commandes Chat ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# Helper: run a chat command and check for a chat message
test_chat_cmd() {
local id="$1" cmd="$2" pattern="$3"
local before after
before=$(jse_get_async "return game.messages.size;" | tr -d '"\n')
jse_get_async "
const msg = {content: '$cmd', speaker: {alias: 'TC-16'}};
game.system.rdd.commands.onChatMessage('$cmd', msg);
await new Promise(r => setTimeout(r, 1500));
return 'done';
" >/dev/null 2>&1
after=$(jse_get_async "return game.messages.size;" | tr -d '"\n')
local newcount=$((after - before))
local match
match=$(jse_get_async "
const msgs = game.messages.contents.slice(-$newcount);
const found = msgs.some(m => (m.content || '').match(/$pattern/));
return found;
" | tr -d '"\n')
[ "$match" = "true" ] && pass "$id: $cmd → match '$pattern'" || fail "$id: $cmd → no match (new=$newcount)"
}
# TC-16-001: /aide
echo "--- TC-16-001: /aide ---"
test_chat_cmd "TC-16-001" "/aide" "aide|help|command"
# TC-16-003: /table queue
echo "--- TC-16-003: /table queue ---"
test_chat_cmd "TC-16-003" "/table queue" "Queue|queue|dragon"
# TC-16-004: /table ombre
echo "--- TC-16-004: /table ombre ---"
test_chat_cmd "TC-16-004" "/table ombre" "Ombre|ombre|Thanatos"
# TC-16-005: /table tete
echo "--- TC-16-005: /table tete ---"
test_chat_cmd "TC-16-005" "/table tete" "[Tt]ête|Tete|Dragon|tirage"
# TC-16-006: /table souffle
echo "--- TC-16-006: /table souffle ---"
test_chat_cmd "TC-16-006" "/table souffle" "Souffle|souffle"
# TC-16-007: /table tarot
echo "--- TC-16-007: /table tarot ---"
test_chat_cmd "TC-16-007" "/table tarot" "[Tt]arot"
# TC-16-017: /meteo
echo "--- TC-16-017: /meteo ---"
test_chat_cmd "TC-16-017" "/meteo" "[Mm]étéo|[Vv]ent|[Mm]er|[Nn]uage|[Pp]luie"
# TC-16-018: /nom
echo "--- TC-16-018: /nom ---"
test_chat_cmd "TC-16-018" "/nom" "nom|Nom|proposition|Créer"
# TC-16-023: /ddr
echo "--- TC-16-023: /ddr ---"
test_chat_cmd "TC-16-023" "/ddr" "draconique|Dé|d[ée]|[Dd]ragon"
# TC-16-029: /chrono
echo "--- TC-16-029: /chrono ---"
R=$(jse_get_async "
try {
game.system.rdd.commands.onChatMessage('/chrono', {content: '/chrono', speaker: {alias: 'TC-16'}});
await new Promise(r => setTimeout(r, 2000));
const el = document.querySelector('.dialog-chronologie, [class*=chronolog]');
return { opened: !!el };
} catch(e) { return {error: e.message}; }
")
OPENED=$(eval_ok "$R" opened)
ERR=$(eval_ok "$R" error)
[ "$OPENED" = "True" ] && pass "TC-16-029: /chrono dialog ouvert" || fail "TC-16-029: KO ($ERR)"
# Close chrono
jse_get_async "
const apps = Object.values(ui.windows).filter(w => w.constructor?.name?.includes('Chronolog'));
for (const a of apps) await a.close();
return 'closed';
" >/dev/null 2>&1
# ============================================================================
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 10 ] 2>/dev/null && pass "Console errors ≤10 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# ============================================================================
# TC-17 — Token HUD
# Usage: ./tc-17-token-hud.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-17 — Token HUD ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-17-006: HUD token non-combat (pas d'erreur) ---"
# ============================================================================
R=$(jse_get_async "
try {
const { RdDTokenHud } = await import('/systems/foundryvtt-reve-de-dragon/module/rdd-token-hud.js');
if (!RdDTokenHud) return {error: 'no RdDTokenHud'};
const hasInit = typeof RdDTokenHud.init === 'function';
const hasOnRender = typeof RdDTokenHud.onRenderTokenHUD === 'function';
return { hasInit, hasOnRender, ok: hasInit || hasOnRender };
} catch(e) { return {error: e.message}; }
")
OK=$(eval_ok "$R" ok)
ERR=$(eval_ok "$R" error)
[ "$OK" = "True" ] && pass "TC-17-006: RdDTokenHud chargé" || fail "TC-17-006: KO ($ERR)"
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# ============================================================================
# TC-18 — Dialogues
# Usage: ./tc-18-dialogues.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-18 — Dialogues ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# Setup
SETUP=$(jse_get_async "
const ex = game.actors.getName('TC-18 Test');
if (ex) await ex.delete();
const [actor] = await Actor.createDocuments([{type:'personnage', name:'TC-18 Test'}]);
return { actorId: actor.id };
")
AID=$(eval_ok "$SETUP" actorId)
# ============================================================================
echo "--- TC-18-014: Dialogues V2 — window positioning ---"
# ============================================================================
R=$(jse_get_async "
try {
// Open a DialogV2 and check it has window controls (position, resize)
const dlg = new foundry.applications.api.DialogV2({
title: 'TC-18 Test Dialog',
content: '<p>Test</p>',
buttons: [{action: 'close', label: 'Fermer', callback: (e,b) => b.close()}]
});
await dlg.render(true);
await new Promise(r => setTimeout(r, 1000));
const el = dlg.element;
const hasWindow = !!el?.querySelector('.window-header');
const hasControls = !!el?.querySelector('.window-controls, .close, .header-button');
await dlg.close();
return { hasWindow, hasControls };
} catch(e) { return {error: e.message}; }
")
HASWIN=$(eval_ok "$R" hasWindow)
ERR=$(eval_ok "$R" error)
[ "$HASWIN" = "True" ] && pass "TC-18-014: dialog V2 avec window header" || fail "TC-18-014: KO ($ERR)"
# ============================================================================
echo "--- TC-18-015: Dialogues V2 — boutons (au moins 1 bouton) ---"
# ============================================================================
R=$(jse_get_async "
try {
const dlg = new foundry.applications.api.DialogV2({
title: 'TC-18 Test Buttons',
content: '<p>Test</p>',
buttons: [{action: 'ok', label: 'OK', callback: (e,b) => b.close()}]
});
await dlg.render(true);
await new Promise(r => setTimeout(r, 1000));
const btns = dlg.element?.querySelectorAll('button');
const count = btns?.length ?? 0;
await dlg.close();
return { btnCount: count, ok: count >= 1 };
} catch(e) { return {error: e.message}; }
")
OK=$(eval_ok "$R" ok)
ERR=$(eval_ok "$R" error)
[ "$OK" = "True" ] && pass "TC-18-015: dialog V2 a ≥1 bouton" || fail "TC-18-015: KO ($ERR)"
# Cleanup
jse_get_async "const a = game.actors.getName('TC-18 Test'); if (a) await a.delete(); return 'ok';" >/dev/null 2>&1
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# ============================================================================
# TC-19 — Tirage (FenetreRechercheTirage)
# Usage: ./tc-19-tirage.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-19 — Tirage (FenetreRechercheTirage) ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-19-001: Ouverture /tirage ---"
# ============================================================================
R=$(jse_get_async "
try {
game.system.rdd.commands.onChatMessage('/tirage', {content: '/tirage', speaker: {alias: 'TC-19'}});
await new Promise(r => setTimeout(r, 2000));
const el = document.querySelector('[class*=tirage], [class*=recherche-tirage], .fenetre-recherche-tirage');
return { opened: !!el };
} catch(e) { return {error: e.message}; }
")
OPENED=$(eval_ok "$R" opened)
ERR=$(eval_ok "$R" error)
[ "$OPENED" = "True" ] && pass "TC-19-001: fenêtre tirage ouverte" || fail "TC-19-001: KO ($ERR)"
# Close
jse_get_async "
const apps = Object.values(ui.windows).filter(w => w.constructor?.name?.includes('Tirage') || w.constructor?.name?.includes('Recherche'));
for (const a of apps) await a.close();
return 'closed';
" >/dev/null 2>&1
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# ============================================================================
# TC-20 — Génération Aléatoire de Personnage
# Usage: ./tc-20-gen-aleatoire.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-20 — Génération Aléatoire de Personnage ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-20-003: Génération nom ---"
# ============================================================================
R=$(jse_get_async "
try {
const { RdDNameGen } = await import('/systems/foundryvtt-reve-de-dragon/module/rdd-namegen.js');
if (!RdDNameGen) return {error: 'no RdDNameGen'};
const name = RdDNameGen.generate?.() ?? await RdDNameGen.generate();
return { hasName: !!name, name: String(name).substring(0, 50) };
} catch(e) { return {error: e.message}; }
")
HASNAME=$(eval_ok "$R" hasName)
NAME=$(eval_ok "$R" name)
ERR=$(eval_ok "$R" error)
[ "$HASNAME" = "True" ] && pass "TC-20-003: nom généré = $NAME" || fail "TC-20-003: KO ($ERR)"
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# ============================================================================
# TC-22 — Compendiums
# Usage: ./tc-22-compendiums.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-22 — Compendiums ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-22-001: Packs chargés ---"
# ============================================================================
R=$(jse_get_async "
try {
const packs = game.packs.filter(p => p.metadata?.system === 'foundryvtt-reve-de-dragon');
return { count: packs.length, names: packs.map(p => p.metadata?.name).join(',') };
} catch(e) { return {error: e.message}; }
")
COUNT=$(eval_ok "$R" count)
ERR=$(eval_ok "$R" error)
assert_gt "TC-22-001: ≥10 packs chargés ($COUNT)" "$COUNT" 9
# ============================================================================
echo "--- TC-22-002: Compendium Compétences ---"
# ============================================================================
R=$(jse_get_async "
try {
const packs = game.packs.filter(p => p.metadata?.system === 'foundryvtt-reve-de-dragon');
const compPack = packs.find(p => p.metadata?.name?.includes('competence') || p.metadata?.label?.toLowerCase().includes('comp'));
if (!compPack) return {error: 'no competence pack', names: packs.map(p=>p.metadata.name).join(',')};
const docs = await compPack.getDocuments();
return { count: docs.length, valid: docs.length > 10 };
} catch(e) { return {error: e.message}; }
")
CCOUNT=$(eval_ok "$R" count)
CVALID=$(eval_ok "$R" valid)
ERR=$(eval_ok "$R" error)
[ "$CVALID" = "True" ] && pass "TC-22-002: compétences pack = $CCOUNT items" || fail "TC-22-002: KO ($ERR)"
# ============================================================================
echo "--- TC-22-003: Compendium Sorts ---"
# ============================================================================
R=$(jse_get_async "
try {
const packs = game.packs.filter(p => p.metadata?.system === 'foundryvtt-reve-de-dragon');
const sortPack = packs.find(p => p.metadata?.name?.includes('sort') || p.metadata?.label?.toLowerCase().includes('sort'));
if (!sortPack) return {error: 'no sort pack'};
const docs = await sortPack.getDocuments();
return { count: docs.length, valid: docs.length > 20 };
} catch(e) { return {error: e.message}; }
")
SCOUNT=$(eval_ok "$R" count)
SVALID=$(eval_ok "$R" valid)
ERR=$(eval_ok "$R" error)
[ "$SVALID" = "True" ] && pass "TC-22-003: sorts pack = $SCOUNT items" || fail "TC-22-003: KO ($ERR)"
# ============================================================================
echo "--- TC-22-004: Compendium Équipement ---"
# ============================================================================
R=$(jse_get_async "
try {
const packs = game.packs.filter(p => p.metadata?.system === 'foundryvtt-reve-de-dragon');
const eqPack = packs.find(p => p.metadata?.name?.includes('equipement') || p.metadata?.name?.includes('arme') || p.metadata?.label?.toLowerCase().includes('quip'));
if (!eqPack) return {error: 'no equipement pack'};
const docs = await eqPack.getDocuments();
return { count: docs.length, valid: docs.length > 5 };
} catch(e) { return {error: e.message}; }
")
ECOUNT=$(eval_ok "$R" count)
EVALID=$(eval_ok "$R" valid)
ERR=$(eval_ok "$R" error)
[ "$EVALID" = "True" ] && pass "TC-22-004: équipement pack = $ECOUNT items" || fail "TC-22-004: KO ($ERR)"
# ============================================================================
echo "--- TC-22-006: Drag compendium → acteur ---"
# ============================================================================
R=$(jse_get_async "
try {
const packs = game.packs.filter(p => p.metadata?.system === 'foundryvtt-reve-de-dragon');
// Find an item pack
const itemPack = packs.find(p => p.documentName === 'Item');
if (!itemPack) return {error: 'no item pack'};
const docs = await itemPack.getDocuments();
if (docs.length === 0) return {error: 'empty pack'};
// Create a test actor
const [actor] = await Actor.createDocuments([{type:'personnage', name:'TC-22 Test'}]);
// Get first item from pack
const item = docs[0];
// Clone it to actor
const [created] = await actor.createEmbeddedDocuments('Item', [item.toObject()]);
const has = actor.items.has(created.id);
await actor.delete();
return { dragOk: has, itemName: item.name };
} catch(e) { return {error: e.message}; }
")
DRAGOK=$(eval_ok "$R" dragOk)
ERR=$(eval_ok "$R" error)
[ "$DRAGOK" = "True" ] && pass "TC-22-006: drag compendium → acteur" || fail "TC-22-006: KO ($ERR)"
# ============================================================================
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# ============================================================================
# TC-23 — CSS / UI / Layout
# Usage: ./tc-23-css-ui.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-23 — CSS / UI / Layout ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-23-001: Police CaslonAntique chargée ---"
# ============================================================================
R=$(jse_get "
const cs = window.getComputedStyle(document.body);
const ff = cs.fontFamily || '';
return { fontFamily: ff.substring(0, 200), hasFont: ff.length > 0 };
")
HASFONT=$(eval_ok "$R" hasFont)
[ "$HASFONT" = "True" ] && pass "TC-23-001: police système chargée" || fail "TC-23-001: KO"
# ============================================================================
echo "--- TC-23-006: Sélecteurs natifs (pas de :input) ---"
# ============================================================================
R=$(jse_get_async "
try {
// Check all system CSS files for :input selector
const sheets = Array.from(document.styleSheets);
let hasInputSelector = false;
for (const sheet of sheets) {
try {
const rules = sheet.cssRules;
for (const rule of rules) {
if (rule.selectorText && rule.selectorText.includes(':input')) {
hasInputSelector = true;
break;
}
}
} catch(e) {} // cross-origin
}
return { hasInputSelector, ok: !hasInputSelector };
} catch(e) { return {error: e.message}; }
")
OK=$(eval_ok "$R" ok)
ERR=$(eval_ok "$R" error)
[ "$OK" = "True" ] && pass "TC-23-006: pas de sélecteur :input" || fail "TC-23-006: KO ($ERR)"
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# ============================================================================
# TC-25 — Hotbar
# Usage: ./tc-25-hotbar.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-25 — Hotbar ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-25-001: RdDHotbar module chargé ---"
# ============================================================================
R=$(jse_get_async "
try {
const { RdDHotbar } = await import('/systems/foundryvtt-reve-de-dragon/module/rdd-hotbar-drop.js');
return { hasHotbar: !!RdDHotbar, hasOnDrop: typeof RdDHotbar?.onDropItem === 'function' || typeof RdDHotbar?.onHotbarDrop === 'function' };
} catch(e) { return {error: e.message}; }
")
HASHB=$(eval_ok "$R" hasHotbar)
ERR=$(eval_ok "$R" error)
[ "$HASHB" = "True" ] && pass "TC-25-001: RdDHotbar chargé" || fail "TC-25-001: KO ($ERR)"
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# ============================================================================
# TC-26 — Possession V2
# Usage: ./tc-26-possession.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-26 — Possession V2 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-26-001: RdDPossession module chargé ---"
# ============================================================================
R=$(jse_get_async "
try {
const { RdDPossession } = await import('/systems/foundryvtt-reve-de-dragon/module/combat/rdd-possession.js');
if (!RdDPossession) return {error: 'no RdDPossession'};
const hasAttack = typeof RdDPossession.attack === 'function' || typeof RdDPossession.onAttaquePossession === 'function';
return { hasAttack, ok: !!RdDPossession };
} catch(e) { return {error: e.message}; }
")
OK=$(eval_ok "$R" ok)
ERR=$(eval_ok "$R" error)
[ "$OK" = "True" ] && pass "TC-26-001: RdDPossession chargé" || fail "TC-26-001: KO ($ERR)"
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
# ============================================================================
# TC-27 — Active Effects
# Usage: ./tc-27-active-effects.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-27 — Active Effects ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# Setup
SETUP=$(jse_get_async "
const ex = game.actors.getName('TC-27 Test');
if (ex) await ex.delete();
const [actor] = await Actor.createDocuments([{
type: 'personnage', name: 'TC-27 Test',
system: {
carac: { force: {value: 12} },
sante: { vie: {value: 12, max: 12}, endurance: {value: 20, max: 20} }
}
}]);
return { actorId: actor.id };
")
AID=$(eval_ok "$SETUP" actorId)
# ============================================================================
echo "--- TC-27-001: Création effet ---"
# ============================================================================
R=$(jse_get_async "
try {
const a = game.actors.get('$AID');
if (!a) return {error: 'no actor'};
const effect = await ActiveEffect.create({
name: 'TC-27 Test Effect',
changes: [{ key: 'system.sante.vie.value', mode: CONST.ACTIVE_EFFECT_MODES.ADD, value: 5 }],
}, { parent: a });
return { created: !!effect, id: effect?.id, name: effect?.name };
} catch(e) { return {error: e.message}; }
")
CREATED=$(eval_ok "$R" created)
ERR=$(eval_ok "$R" error)
[ "$CREATED" = "True" ] && pass "TC-27-001: effet créé" || fail "TC-27-001: KO ($ERR)"
# ============================================================================
echo "--- TC-27-002: Application effet sur acteur ---"
# ============================================================================
R=$(jse_get_async "
try {
const a = game.actors.get('$AID');
if (!a) return {error: 'no actor'};
// Effect should have added 5 to vie.value — but ActiveEffect application
// might compute differently. Just check the effect exists and is active.
const hasEffect = a.effects.some(e => e.name === 'TC-27 Test Effect');
const isActive = a.effects.some(e => e.name === 'TC-27 Test Effect' && !e.disabled);
const vie = a.system.sante?.vie?.value;
return { vie, hasEffect, isActive, applied: isActive };
} catch(e) { return {error: e.message}; }
")
VIE=$(eval_ok "$R" vie)
APPLIED=$(eval_ok "$R" applied)
ERR=$(eval_ok "$R" error)
[ "$APPLIED" = "True" ] && pass "TC-27-002: vie = $VIE (effet appliqué)" || fail "TC-27-002: KO ($ERR)"
# ============================================================================
echo "--- TC-27-003: Suppression effet ---"
# ============================================================================
R=$(jse_get_async "
try {
const a = game.actors.get('$AID');
if (!a) return {error: 'no actor'};
const effect = a.effects.find(e => e.name === 'TC-27 Test Effect');
if (!effect) return {error: 'no effect'};
await effect.delete();
await new Promise(r => setTimeout(r, 500));
const vieAfter = a.system.sante?.vie?.value;
const stillHas = a.effects.some(e => e.name === 'TC-27 Test Effect');
return { vieAfter, stillHas, restored: !stillHas };
} catch(e) { return {error: e.message}; }
")
RESTORED=$(eval_ok "$R" restored)
ERR=$(eval_ok "$R" error)
[ "$RESTORED" = "True" ] && pass "TC-27-003: effet supprimé, valeur restaurée" || fail "TC-27-003: KO ($ERR)"
# ============================================================================
echo "--- TC-27-004: Effets de statut ---"
# ============================================================================
R=$(jse_get_async "
try {
const effects = game.rdd?.statusEffects ?? CONFIG.statusEffects;
return { count: effects?.length ?? 0, hasStatus: !!effects };
} catch(e) { return {error: e.message}; }
")
COUNT=$(eval_ok "$R" count)
HASSTAT=$(eval_ok "$R" hasStatus)
ERR=$(eval_ok "$R" error)
[ "$HASSTAT" = "True" ] && pass "TC-27-004: statusEffects = $COUNT" || fail "TC-27-004: KO ($ERR)"
# ============================================================================
echo ""
echo "--- Cleanup ---"
CLEANUP=$(jse_get_async "
const a = game.actors.getName('TC-27 Test');
if (a) { await a.delete(); return 'deleted'; }
return 'none';
" | tr -d '"\n')
[ "$CLEANUP" = "deleted" ] && pass "Cleanup: actor deleted" || fail "Cleanup: $CLEANUP"
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# ============================================================================
# TC-28 — Journal Sheet
# Usage: ./tc-28-journal.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-28 — Journal Sheet ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-28-001: Journal système s'ouvre ---"
# ============================================================================
R=$(jse_get_async "
try {
const journal = await JournalEntry.create({name: 'TC-28 Test', content: '<p>Test</p>'}, {temporary: true});
if (!journal) return {error: 'create failed'};
const sheet = journal.sheet;
await sheet.render(true);
await new Promise(r => setTimeout(r, 1500));
const el = sheet.element;
const ok = !!el && document.body.contains(el);
await sheet.close();
return { opened: ok, sheetName: sheet?.constructor?.name };
} catch(e) { return {error: e.message}; }
")
OPENED=$(eval_ok "$R" opened)
ERR=$(eval_ok "$R" error)
[ "$OPENED" = "True" ] && pass "TC-28-001: journal sheet ouverte" || fail "TC-28-001: KO ($ERR)"
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# ============================================================================
# TC-29 — Cœur (Attachment)
# Usage: ./tc-29-coeur.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-29 — Cœur (Attachment) ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-29-001: RdDCoeur module chargé ---"
# ============================================================================
R=$(jse_get_async "
try {
const { RdDCoeur } = await import('/systems/foundryvtt-reve-de-dragon/module/coeur/rdd-coeur.js');
return { hasCoeur: !!RdDCoeur, hasCreate: typeof RdDCoeur?.create === 'function' };
} catch(e) { return {error: e.message}; }
")
HASCOEUR=$(eval_ok "$R" hasCoeur)
ERR=$(eval_ok "$R" error)
[ "$HASCOEUR" = "True" ] && pass "TC-29-001: RdDCoeur chargé" || fail "TC-29-001: KO ($ERR)"
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# ============================================================================
# TC-30 — Appréciation
# Usage: ./tc-30-appreciation.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-30 — Appréciation ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-30-001: Apprecier module chargé ---"
# ============================================================================
R=$(jse_get_async "
try {
// Appreciation is not a separate module — check actor has appreciation methods
const a = game.actors.filter(a => a.type === 'personnage')[0];
if (!a) return {error: 'no actor'};
const hasAppreciation = typeof a.apprecier === 'function' || typeof a.getAppreciation === 'function' || a.items.some(i => i.system?.appreciation !== undefined);
return { hasApprecier: hasAppreciation };
} catch(e) { return {error: e.message}; }
")
HAS=$(eval_ok "$R" hasApprecier)
ERR=$(eval_ok "$R" error)
[ "$HAS" = "True" ] && pass "TC-30-001: Apprecier chargé" || fail "TC-30-001: KO ($ERR)"
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# ============================================================================
# TC-31 — Météo
# Usage: ./tc-31-meteo.sh [--navigate]
# ============================================================================
set -uo pipefail
source "$(dirname "$0")/tc-header.sh"
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ TC-31 — Météo ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
tc_ensure_ready
# ============================================================================
echo "--- TC-31-001: Génération météo ---"
# ============================================================================
R=$(jse_get_async "
try {
const { RdDMeteo } = await import('/systems/foundryvtt-reve-de-dragon/module/rdd-meteo.js');
if (!RdDMeteo) return {error: 'no RdDMeteo'};
const meteo = RdDMeteo.getMeteo?.() ?? await RdDMeteo.getMeteo();
return { hasMeteo: !!meteo, keys: meteo ? Object.keys(meteo).join(',') : '', hasVent: !!meteo?.vent };
} catch(e) { return {error: e.message}; }
")
HASMETEO=$(eval_ok "$R" hasMeteo)
ERR=$(eval_ok "$R" error)
[ "$HASMETEO" = "True" ] && pass "TC-31-001: météo générée" || fail "TC-31-001: KO ($ERR)"
# ============================================================================
echo "--- TC-31-002: Affichage météo (/meteo) ---"
# ============================================================================
BEFORE=$(jse_get_async "return game.messages.size;" | tr -d '"\n')
jse_get_async "
game.system.rdd.commands.onChatMessage('/meteo', {content: '/meteo', speaker: {alias: 'TC-31'}});
await new Promise(r => setTimeout(r, 1500));
return 'done';
" >/dev/null 2>&1
AFTER=$(jse_get_async "return game.messages.size;" | tr -d '"\n')
NEWMGS=$((AFTER - BEFORE))
MATCH=$(jse_get_async "
const msgs = game.messages.contents.slice(-$NEWMGS);
const found = msgs.some(m => (m.content || '').match(/[Mm]étéo|[Vv]ent|[Mm]er/));
return found;
" | tr -d '"\n')
[ "$MATCH" = "true" ] && pass "TC-31-002: /meteo message chat" || fail "TC-31-002: KO (new=$NEWMGS)"
echo ""
echo "--- Console errors ---"
ERRS=$(tc_console_errors)
[ "$ERRS" -le 5 ] 2>/dev/null && pass "Console errors ≤5 ($ERRS)" || fail "Console errors: $ERRS"
tc_summary
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# ============================================================================
# tc-header.sh — fonctions partagées pour tous les scripts TC
# Source: source "$(dirname "$0")/tc-header.sh"
# ============================================================================
# Constants
URL="${URL:-https://localhost:31000}"
PASS="${PASS:-}"
WORLD="${WORLD:-Reve de Dragon}"
NAVIGATE="${1:-}"
PASSED=0; FAILED=0
die() { echo "$*"; exit 1; }
pass() { echo "$1"; PASSED=$((PASSED+1)); }
fail() { echo "$1"; FAILED=$((FAILED+1)); }
# Extract JSON value from evaluate_script output (```json\nVALUE\n```)
jse_get() {
chrome-devtools evaluate_script "() => { $1 }" 2>/dev/null \
| sed -n '/^```json$/{n;p;}'
}
jse_get_async() {
chrome-devtools evaluate_script "async () => { $1 }" 2>/dev/null \
| sed -n '/^```json$/{n;p;}'
}
# Async polling: single evaluate_script call, no bash loop
jse_wait_ready() {
local timeout_s="${1:-60}" poll_ms="${2:-1000}"
chrome-devtools evaluate_script "async () => {
const limit = $timeout_s * 1000 / $poll_ms;
for (let i = 0; i < limit; i++) {
if (typeof game !== 'undefined' && game.ready) return 'ready';
await new Promise(r => setTimeout(r, $poll_ms));
}
return 'timeout';
}" 2>/dev/null | sed -n '/^```json$/{n;p;}' | tr -d '"\n'
}
# Navigate + login if needed (--navigate flag)
# Env: PASS (admin password), PASS_USER (user password, empty by default)
PASS_USER="${PASS_USER:-}"
tc_login() {
[ -z "$PASS" ] && die "Admin password required: PASS=..."
echo "→ Navigating to $URL ..."
chrome-devtools new_page "$URL" --background false --timeout 15000 --isolatedContext "rdd-tests" >/dev/null 2>&1
sleep 3
local PW
PW=$(jse_get "window.location.pathname" | tr -d '"\n')
case "$PW" in
*/join)
chrome-devtools evaluate_script "() => {
const sel = document.querySelector('select[name=userid]');
if(sel) {
const opts = Array.from(sel.options);
const gm = opts.find(o => o.text === 'Gamemaster');
const pick = gm || opts.find(o => o.value && !o.disabled) || opts.find(o => o.value);
if(pick) sel.value = pick.value;
}
const pw = document.querySelector('input[name=password]');
if(pw) pw.value = '$PASS_USER';
const btn = document.querySelector('button[name=join]');
if(btn) btn.click();
}" >/dev/null 2>&1 || true
;;
*/setup)
chrome-devtools evaluate_script "() => {
const links = document.querySelectorAll('[data-action=worldLaunch]');
for(const l of links){
const li = l.closest('li.package.world');
if(li && li.querySelector('.package-title')?.textContent === '$WORLD'){ l.click(); break; }
}
}" >/dev/null 2>&1 || true
;;
*/auth)
chrome-devtools evaluate_script "() => {
const inp = document.querySelector('input[type=password]');
if(inp) inp.value = '$PASS';
const btn = document.querySelector('form button, button[type=submit]');
if(btn) btn.click();
}" >/dev/null 2>&1 || true
;;
esac
echo "→ Waiting for game..."
jse_wait_ready 60 2
}
# Ensure game is ready, login if --navigate
tc_ensure_ready() {
local R
R=$(jse_wait_ready 15 2)
if [ "$R" != "ready" ] && [ "$NAVIGATE" = "--navigate" ]; then
R=$(tc_login)
fi
[ "$R" != "ready" ] && die "Game not ready"
echo " ✓ Game ready"
echo ""
}
# Assert helpers
assert_eq() { local t="$1" a="$2" e="$3"
if [ "$a" = "$e" ]; then pass "$t"; else fail "$t (got '$a' expected '$e')"; fi
}
assert_gt() { local t="$1" a="$2" e="$3"
if [ "$a" -gt "$e" ] 2>/dev/null; then pass "$t"; else fail "$t ($a <= $e)"; fi
}
assert_contains() { local t="$1" a="$2" e="$3"
if echo "$a" | grep -q "$e"; then pass "$t"; else fail "$t (no '$e' in '$a')"; fi
}
# Extract field from JSON result
eval_ok() {
local json="$1" k="$2"
echo "$json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$k',''))" 2>/dev/null
}
# Console error count
tc_console_errors() {
chrome-devtools list_console_messages --types error 2>/dev/null | grep -c '^msgid=' || true
}
# Print summary
tc_summary() {
local TOTAL=$((PASSED + FAILED))
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
printf "║ %2d / %2d passed, %2d failed ║\n" "$PASSED" "$TOTAL" "$FAILED"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
[ "$FAILED" -gt 0 ] && exit 1 || exit 0
}