8 changed files with 1835 additions and 14 deletions
@ -0,0 +1,27 @@ |
|||||||
|
{ |
||||||
|
"name": "birthday-spire", |
||||||
|
"version": "1.0.0", |
||||||
|
"description": "A Slay the Spire-inspired web-based card game as a humorous birthday present", |
||||||
|
"type": "module", |
||||||
|
"scripts": { |
||||||
|
"test": "node tests/run-tests.js", |
||||||
|
"test:verbose": "node tests/run-tests.js --verbose", |
||||||
|
"test:coverage": "node tests/run-tests.js --coverage", |
||||||
|
"test:card": "node tests/run-tests.js --card=", |
||||||
|
"serve": "python3 -m http.server 8002", |
||||||
|
"dev": "python3 -m http.server 8002" |
||||||
|
}, |
||||||
|
"keywords": [ |
||||||
|
"game", |
||||||
|
"card-game", |
||||||
|
"web-game", |
||||||
|
"birthday", |
||||||
|
"humor" |
||||||
|
], |
||||||
|
"author": "Birthday Spire Team", |
||||||
|
"license": "MIT", |
||||||
|
"devDependencies": {}, |
||||||
|
"engines": { |
||||||
|
"node": ">=14.0.0" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,162 @@ |
|||||||
|
# Birthday Spire Test Suite |
||||||
|
|
||||||
|
This directory contains comprehensive tests for all card mechanics in Birthday Spire. |
||||||
|
|
||||||
|
## Files |
||||||
|
|
||||||
|
- **`card-tests.js`** - Complete test suite with all card tests |
||||||
|
- **`run-tests.js`** - Node.js command-line test runner |
||||||
|
- **`test-runner.html`** - Browser-based test runner with UI |
||||||
|
- **`README.md`** - This documentation |
||||||
|
|
||||||
|
## Running Tests |
||||||
|
|
||||||
|
### Command Line (Node.js) |
||||||
|
|
||||||
|
```bash |
||||||
|
# Run all tests |
||||||
|
npm test |
||||||
|
|
||||||
|
# Run tests with verbose output |
||||||
|
npm run test:verbose |
||||||
|
|
||||||
|
# Check test coverage |
||||||
|
npm run test:coverage |
||||||
|
|
||||||
|
# Test a specific card |
||||||
|
npm run test:card strike |
||||||
|
|
||||||
|
# Or directly with node |
||||||
|
node tests/run-tests.js |
||||||
|
node tests/run-tests.js --verbose |
||||||
|
node tests/run-tests.js --coverage |
||||||
|
node tests/run-tests.js --card=strike |
||||||
|
``` |
||||||
|
|
||||||
|
### Browser |
||||||
|
|
||||||
|
1. Start the development server: |
||||||
|
```bash |
||||||
|
npm run serve |
||||||
|
``` |
||||||
|
|
||||||
|
2. Open `http://localhost:8002/tests/test-runner.html` in your browser |
||||||
|
|
||||||
|
3. Use the interactive test runner: |
||||||
|
- **Run All Tests** - Executes sample test suite |
||||||
|
- **Check Coverage** - Shows test coverage statistics |
||||||
|
- **Test Specific Card** - Test any card by ID |
||||||
|
- **Clear Output** - Clears the test output |
||||||
|
|
||||||
|
## Test Structure |
||||||
|
|
||||||
|
Each test follows this pattern: |
||||||
|
|
||||||
|
```javascript |
||||||
|
testCard('cardId', setupFn, assertionFn) |
||||||
|
``` |
||||||
|
|
||||||
|
- **`cardId`** - The card to test |
||||||
|
- **`setupFn(ctx)`** - Optional setup function to modify test context |
||||||
|
- **`assertionFn(result, ctx)`** - Function to verify the test results |
||||||
|
|
||||||
|
### Example Test |
||||||
|
|
||||||
|
```javascript |
||||||
|
strike: () => testCard('strike', null, (result) => { |
||||||
|
if (result.finalState.enemyHp !== 94) { |
||||||
|
throw new Error(`Expected enemy HP 94, got ${result.finalState.enemyHp}`); |
||||||
|
} |
||||||
|
}) |
||||||
|
``` |
||||||
|
|
||||||
|
## Test Context |
||||||
|
|
||||||
|
The test context provides a mock battle environment: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ |
||||||
|
player: { hp, maxHp, energy, block, hand, deck, draw, discard, ... }, |
||||||
|
enemy: { hp, maxHp, block, weak, vuln, intent, ... }, |
||||||
|
flags: { skipThisTurn, nextCardFree, doubleNextCard, ... }, |
||||||
|
logs: [], // Captured log messages |
||||||
|
|
||||||
|
// Available functions: |
||||||
|
deal(target, amount), |
||||||
|
draw(n), |
||||||
|
applyWeak(target, amount), |
||||||
|
applyVulnerable(target, amount), |
||||||
|
intentIsAttack(), |
||||||
|
scalarFromWeak(base), |
||||||
|
forceEndTurn(), |
||||||
|
promptExhaust(count), |
||||||
|
moveFromDiscardToHand(cardId), |
||||||
|
countCardType(type), |
||||||
|
replayCard(card) |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Adding New Tests |
||||||
|
|
||||||
|
To add a test for a new card: |
||||||
|
|
||||||
|
1. Add the test to the `tests` object in `run-tests.js`: |
||||||
|
|
||||||
|
```javascript |
||||||
|
my_new_card: () => testCard('my_new_card', |
||||||
|
(ctx) => { |
||||||
|
// Setup test conditions |
||||||
|
ctx.player.hp = 30; |
||||||
|
ctx.enemy.block = 5; |
||||||
|
}, |
||||||
|
(result, ctx) => { |
||||||
|
// Assert expected results |
||||||
|
if (result.finalState.enemyHp !== expectedHp) { |
||||||
|
throw new Error(`Expected enemy HP ${expectedHp}, got ${result.finalState.enemyHp}`); |
||||||
|
} |
||||||
|
} |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
## Test Coverage |
||||||
|
|
||||||
|
Current test coverage includes: |
||||||
|
|
||||||
|
- ✅ Basic attack/skill cards (strike, defend, etc.) |
||||||
|
- ✅ Energy manipulation cards (coffee_rush, etc.) |
||||||
|
- ✅ Complex effect cards (macro, segfault, etc.) |
||||||
|
- ✅ Flag-based cards (just_one_game, pair_programming, etc.) |
||||||
|
- ✅ Advanced mechanics (ctrl_z, npm_audit, infinite_loop, etc.) |
||||||
|
- ✅ Healing cards (stack_trace, refactor, etc.) |
||||||
|
- ✅ Multi-hit cards (merge_conflict, etc.) |
||||||
|
- ✅ Self-damage cards (production_deploy, etc.) |
||||||
|
|
||||||
|
Run `npm run test:coverage` to see detailed coverage statistics. |
||||||
|
|
||||||
|
## Debugging Tests |
||||||
|
|
||||||
|
For debugging failed tests: |
||||||
|
|
||||||
|
1. Use `--verbose` flag to see detailed logs |
||||||
|
2. Test specific cards with `--card=cardId` |
||||||
|
3. Check the browser test runner for interactive debugging |
||||||
|
4. Examine the test context setup and assertions |
||||||
|
|
||||||
|
## Integration with CI/CD |
||||||
|
|
||||||
|
The test suite returns appropriate exit codes: |
||||||
|
- `0` - All tests passed |
||||||
|
- `1` - One or more tests failed |
||||||
|
|
||||||
|
This makes it suitable for continuous integration systems. |
||||||
|
|
||||||
|
## Extending Tests |
||||||
|
|
||||||
|
To test more complex scenarios: |
||||||
|
|
||||||
|
- **Multi-card combos**: Set up multiple cards in hand/play sequence |
||||||
|
- **Relic interactions**: Add relic states to context |
||||||
|
- **Status effect combinations**: Test weak/vulnerable interactions |
||||||
|
- **Edge cases**: Test with 0 energy, full hand, empty deck, etc. |
||||||
|
|
||||||
|
The test framework is designed to be flexible and extensible for any card mechanics you add to the game. |
||||||
@ -0,0 +1,501 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||||
|
<title>Birthday Spire Card Tests</title> |
||||||
|
<style> |
||||||
|
body { |
||||||
|
font-family: 'Courier New', monospace; |
||||||
|
max-width: 1200px; |
||||||
|
margin: 0 auto; |
||||||
|
padding: 20px; |
||||||
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); |
||||||
|
color: white; |
||||||
|
min-height: 100vh; |
||||||
|
} |
||||||
|
|
||||||
|
.container { |
||||||
|
background: rgba(0, 0, 0, 0.7); |
||||||
|
padding: 30px; |
||||||
|
border-radius: 10px; |
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); |
||||||
|
} |
||||||
|
|
||||||
|
h1 { |
||||||
|
text-align: center; |
||||||
|
color: #ffd700; |
||||||
|
margin-bottom: 30px; |
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); |
||||||
|
} |
||||||
|
|
||||||
|
.controls { |
||||||
|
text-align: center; |
||||||
|
margin-bottom: 30px; |
||||||
|
} |
||||||
|
|
||||||
|
button { |
||||||
|
background: linear-gradient(135deg, #ff6b6b, #ee5a52); |
||||||
|
border: none; |
||||||
|
color: white; |
||||||
|
padding: 12px 24px; |
||||||
|
margin: 0 10px; |
||||||
|
border-radius: 5px; |
||||||
|
cursor: pointer; |
||||||
|
font-size: 16px; |
||||||
|
font-weight: bold; |
||||||
|
transition: transform 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
button:hover { |
||||||
|
transform: translateY(-2px); |
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); |
||||||
|
} |
||||||
|
|
||||||
|
button:disabled { |
||||||
|
background: #666; |
||||||
|
cursor: not-allowed; |
||||||
|
transform: none; |
||||||
|
} |
||||||
|
|
||||||
|
.results { |
||||||
|
background: rgba(0, 0, 0, 0.5); |
||||||
|
padding: 20px; |
||||||
|
border-radius: 5px; |
||||||
|
margin-top: 20px; |
||||||
|
border-left: 4px solid #ffd700; |
||||||
|
} |
||||||
|
|
||||||
|
.test-output { |
||||||
|
background: #000; |
||||||
|
color: #00ff00; |
||||||
|
padding: 20px; |
||||||
|
border-radius: 5px; |
||||||
|
font-family: 'Courier New', monospace; |
||||||
|
font-size: 14px; |
||||||
|
line-height: 1.4; |
||||||
|
max-height: 600px; |
||||||
|
overflow-y: auto; |
||||||
|
white-space: pre-wrap; |
||||||
|
margin-top: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.stats { |
||||||
|
display: flex; |
||||||
|
justify-content: space-around; |
||||||
|
margin: 20px 0; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.stat { |
||||||
|
background: rgba(255, 255, 255, 0.1); |
||||||
|
padding: 15px; |
||||||
|
border-radius: 5px; |
||||||
|
flex: 1; |
||||||
|
margin: 0 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.stat-value { |
||||||
|
font-size: 24px; |
||||||
|
font-weight: bold; |
||||||
|
color: #ffd700; |
||||||
|
} |
||||||
|
|
||||||
|
.stat-label { |
||||||
|
font-size: 14px; |
||||||
|
opacity: 0.8; |
||||||
|
} |
||||||
|
|
||||||
|
.error-details { |
||||||
|
background: rgba(255, 0, 0, 0.1); |
||||||
|
border: 1px solid #ff4444; |
||||||
|
padding: 15px; |
||||||
|
border-radius: 5px; |
||||||
|
margin-top: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.success { color: #00ff00; } |
||||||
|
.error { color: #ff4444; } |
||||||
|
.warning { color: #ffaa00; } |
||||||
|
|
||||||
|
.loading { |
||||||
|
text-align: center; |
||||||
|
color: #ffd700; |
||||||
|
font-style: italic; |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div class="container"> |
||||||
|
<h1>🎮 Birthday Spire Card Tests</h1> |
||||||
|
|
||||||
|
<div class="controls"> |
||||||
|
<button onclick="runTests()" id="runBtn">Run All Tests</button> |
||||||
|
<button onclick="checkCoverage()" id="coverageBtn">Check Coverage</button> |
||||||
|
<button onclick="runSpecificTest()" id="specificBtn">Test Specific Card</button> |
||||||
|
<button onclick="clearOutput()" id="clearBtn">Clear Output</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="stats" id="stats" style="display: none;"> |
||||||
|
<div class="stat"> |
||||||
|
<div class="stat-value" id="passedCount">0</div> |
||||||
|
<div class="stat-label">Passed</div> |
||||||
|
</div> |
||||||
|
<div class="stat"> |
||||||
|
<div class="stat-value" id="failedCount">0</div> |
||||||
|
<div class="stat-label">Failed</div> |
||||||
|
</div> |
||||||
|
<div class="stat"> |
||||||
|
<div class="stat-value" id="coveragePercent">0%</div> |
||||||
|
<div class="stat-label">Coverage</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="test-output" id="output">Ready to run tests...</div> |
||||||
|
|
||||||
|
<div id="errorDetails" class="error-details" style="display: none;"> |
||||||
|
<h3>Failed Tests:</h3> |
||||||
|
<div id="errorList"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- Import the game modules --> |
||||||
|
<script type="module"> |
||||||
|
import { CARDS, STARTER_DECK, CARD_POOL } from '../src/data/cards.js'; |
||||||
|
import { ENEMIES } from '../src/data/enemies.js'; |
||||||
|
import { makePlayer, cloneCard } from '../src/engine/core.js'; |
||||||
|
|
||||||
|
// Make available globally for the test runner |
||||||
|
window.CARDS = CARDS; |
||||||
|
window.ENEMIES = ENEMIES; |
||||||
|
window.makePlayer = makePlayer; |
||||||
|
window.cloneCard = cloneCard; |
||||||
|
|
||||||
|
// Simple console override to capture output |
||||||
|
const originalConsole = { ...console }; |
||||||
|
let capturedOutput = []; |
||||||
|
|
||||||
|
function captureConsole() { |
||||||
|
capturedOutput = []; |
||||||
|
console.log = (...args) => { |
||||||
|
capturedOutput.push(args.join(' ')); |
||||||
|
originalConsole.log(...args); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function restoreConsole() { |
||||||
|
Object.assign(console, originalConsole); |
||||||
|
} |
||||||
|
|
||||||
|
function output(text, className = '') { |
||||||
|
const outputEl = document.getElementById('output'); |
||||||
|
const span = document.createElement('span'); |
||||||
|
span.className = className; |
||||||
|
span.textContent = text + '\n'; |
||||||
|
outputEl.appendChild(span); |
||||||
|
outputEl.scrollTop = outputEl.scrollHeight; |
||||||
|
} |
||||||
|
|
||||||
|
function clearOutput() { |
||||||
|
document.getElementById('output').innerHTML = 'Output cleared...\n'; |
||||||
|
document.getElementById('stats').style.display = 'none'; |
||||||
|
document.getElementById('errorDetails').style.display = 'none'; |
||||||
|
} |
||||||
|
|
||||||
|
function updateStats(results) { |
||||||
|
document.getElementById('passedCount').textContent = results.passed; |
||||||
|
document.getElementById('failedCount').textContent = results.failed; |
||||||
|
document.getElementById('stats').style.display = 'flex'; |
||||||
|
|
||||||
|
if (results.errors && results.errors.length > 0) { |
||||||
|
const errorDetails = document.getElementById('errorDetails'); |
||||||
|
const errorList = document.getElementById('errorList'); |
||||||
|
errorList.innerHTML = ''; |
||||||
|
|
||||||
|
results.errors.forEach(({ cardId, error }) => { |
||||||
|
const errorDiv = document.createElement('div'); |
||||||
|
errorDiv.innerHTML = `<strong>${cardId}:</strong> ${error}`; |
||||||
|
errorList.appendChild(errorDiv); |
||||||
|
}); |
||||||
|
|
||||||
|
errorDetails.style.display = 'block'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Test utilities (simplified version for browser) |
||||||
|
function createTestBattleContext() { |
||||||
|
const player = makePlayer(); |
||||||
|
player.hp = 50; |
||||||
|
player.maxHp = 50; |
||||||
|
player.energy = 3; |
||||||
|
player.maxEnergy = 3; |
||||||
|
player.block = 0; |
||||||
|
player.weak = 0; |
||||||
|
player.vuln = 0; |
||||||
|
player.hand = []; |
||||||
|
player.deck = []; |
||||||
|
player.draw = []; |
||||||
|
player.discard = []; |
||||||
|
|
||||||
|
const enemy = { |
||||||
|
id: "test_dummy", |
||||||
|
name: "Test Dummy", |
||||||
|
hp: 100, |
||||||
|
maxHp: 100, |
||||||
|
block: 0, |
||||||
|
weak: 0, |
||||||
|
vuln: 0, |
||||||
|
intent: { type: "attack", value: 5 } |
||||||
|
}; |
||||||
|
|
||||||
|
const logs = []; |
||||||
|
const ctx = { |
||||||
|
player, |
||||||
|
enemy, |
||||||
|
flags: {}, |
||||||
|
lastCard: null, |
||||||
|
relicStates: [], |
||||||
|
log: (msg) => logs.push(msg), |
||||||
|
logs, |
||||||
|
render: () => {}, |
||||||
|
deal: (target, amount) => { |
||||||
|
const blocked = Math.min(amount, target.block); |
||||||
|
const damage = Math.max(0, amount - blocked); |
||||||
|
target.block -= blocked; |
||||||
|
target.hp -= damage; |
||||||
|
logs.push(`Deal ${amount} damage (${damage} after ${blocked} block)`); |
||||||
|
}, |
||||||
|
draw: (n) => { |
||||||
|
for (let i = 0; i < n && player.draw.length > 0; i++) { |
||||||
|
const cardId = player.draw.pop(); |
||||||
|
const card = cloneCard(CARDS[cardId]); |
||||||
|
if (card) player.hand.push(card); |
||||||
|
} |
||||||
|
}, |
||||||
|
applyWeak: (target, amount) => { |
||||||
|
target.weak = (target.weak || 0) + amount; |
||||||
|
logs.push(`Applied ${amount} weak`); |
||||||
|
}, |
||||||
|
applyVulnerable: (target, amount) => { |
||||||
|
target.vuln = (target.vuln || 0) + amount; |
||||||
|
logs.push(`Applied ${amount} vulnerable`); |
||||||
|
}, |
||||||
|
intentIsAttack: () => enemy.intent.type === "attack", |
||||||
|
scalarFromWeak: (base) => player.weak > 0 ? Math.floor(base * 0.75) : base, |
||||||
|
forceEndTurn: () => logs.push("Turn ended"), |
||||||
|
promptExhaust: (count) => { |
||||||
|
while (count-- > 0 && player.hand.length > 0) { |
||||||
|
const card = player.hand.shift(); |
||||||
|
logs.push(`Exhausted ${card.name}`); |
||||||
|
} |
||||||
|
}, |
||||||
|
moveFromDiscardToHand: (cardId) => { |
||||||
|
const idx = player.discard.findIndex(id => id === cardId); |
||||||
|
if (idx >= 0) { |
||||||
|
const [id] = player.discard.splice(idx, 1); |
||||||
|
const card = cloneCard(CARDS[id]); |
||||||
|
if (card) { |
||||||
|
player.hand.push(card); |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
}, |
||||||
|
countCardType: (type) => { |
||||||
|
const allCards = [...player.deck, ...player.hand.map(c => c.id), ...player.draw, ...player.discard]; |
||||||
|
return allCards.filter(id => CARDS[id]?.type === type).length; |
||||||
|
}, |
||||||
|
replayCard: (card) => { |
||||||
|
if (typeof card.effect === 'function') { |
||||||
|
card.effect(ctx); |
||||||
|
logs.push(`Replayed ${card.name}`); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ctx; |
||||||
|
} |
||||||
|
|
||||||
|
function testCard(cardId, setupFn = null, assertionFn = null) { |
||||||
|
const card = CARDS[cardId]; |
||||||
|
if (!card) { |
||||||
|
throw new Error(`Card ${cardId} not found`); |
||||||
|
} |
||||||
|
|
||||||
|
const ctx = createTestBattleContext(); |
||||||
|
|
||||||
|
if (setupFn) { |
||||||
|
setupFn(ctx); |
||||||
|
} |
||||||
|
|
||||||
|
const initialState = { |
||||||
|
playerHp: ctx.player.hp, |
||||||
|
enemyHp: ctx.enemy.hp, |
||||||
|
playerBlock: ctx.player.block, |
||||||
|
playerEnergy: ctx.player.energy, |
||||||
|
handSize: ctx.player.hand.length |
||||||
|
}; |
||||||
|
|
||||||
|
ctx.logs.length = 0; |
||||||
|
|
||||||
|
try { |
||||||
|
card.effect(ctx); |
||||||
|
|
||||||
|
const finalState = { |
||||||
|
playerHp: ctx.player.hp, |
||||||
|
enemyHp: ctx.enemy.hp, |
||||||
|
playerBlock: ctx.player.block, |
||||||
|
playerEnergy: ctx.player.energy, |
||||||
|
handSize: ctx.player.hand.length |
||||||
|
}; |
||||||
|
|
||||||
|
const result = { |
||||||
|
card, |
||||||
|
initialState, |
||||||
|
finalState, |
||||||
|
logs: [...ctx.logs], |
||||||
|
flags: { ...ctx.flags }, |
||||||
|
success: true, |
||||||
|
error: null |
||||||
|
}; |
||||||
|
|
||||||
|
if (assertionFn) { |
||||||
|
assertionFn(result, ctx); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} catch (error) { |
||||||
|
return { |
||||||
|
card, |
||||||
|
initialState, |
||||||
|
finalState: initialState, |
||||||
|
logs: [...ctx.logs], |
||||||
|
flags: { ...ctx.flags }, |
||||||
|
success: false, |
||||||
|
error: error.message |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Sample tests |
||||||
|
const sampleTests = { |
||||||
|
strike: () => testCard('strike', null, (result) => { |
||||||
|
if (result.finalState.enemyHp !== 94) { |
||||||
|
throw new Error(`Expected enemy to have 94 HP, got ${result.finalState.enemyHp}`); |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
defend: () => testCard('defend', null, (result) => { |
||||||
|
if (result.finalState.playerBlock !== 5) { |
||||||
|
throw new Error(`Expected player to have 5 block, got ${result.finalState.playerBlock}`); |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
coffee_rush: () => testCard('coffee_rush', null, (result) => { |
||||||
|
if (result.finalState.playerEnergy !== 5) { |
||||||
|
throw new Error(`Expected player to have 5 energy, got ${result.finalState.playerEnergy}`); |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
segfault: () => testCard('segfault', |
||||||
|
(ctx) => { |
||||||
|
ctx.player.draw = ['strike']; |
||||||
|
}, |
||||||
|
(result) => { |
||||||
|
if (result.finalState.enemyHp !== 93) { |
||||||
|
throw new Error(`Expected enemy to have 93 HP, got ${result.finalState.enemyHp}`); |
||||||
|
} |
||||||
|
if (result.finalState.handSize !== 1) { |
||||||
|
throw new Error(`Expected 1 card drawn, got ${result.finalState.handSize}`); |
||||||
|
} |
||||||
|
} |
||||||
|
), |
||||||
|
|
||||||
|
pair_programming: () => testCard('pair_programming', null, (result) => { |
||||||
|
if (!result.flags.doubleNextCard) { |
||||||
|
throw new Error(`Expected doubleNextCard flag to be set`); |
||||||
|
} |
||||||
|
}) |
||||||
|
}; |
||||||
|
|
||||||
|
window.runTests = async function() { |
||||||
|
clearOutput(); |
||||||
|
output('🎮 Running Birthday Spire Card Tests...', 'success'); |
||||||
|
output(''); |
||||||
|
|
||||||
|
const results = { |
||||||
|
passed: 0, |
||||||
|
failed: 0, |
||||||
|
errors: [] |
||||||
|
}; |
||||||
|
|
||||||
|
for (const [cardId, testFn] of Object.entries(sampleTests)) { |
||||||
|
try { |
||||||
|
output(`Testing ${cardId}...`); |
||||||
|
const result = testFn(); |
||||||
|
output(`✅ ${cardId} passed`, 'success'); |
||||||
|
results.passed++; |
||||||
|
} catch (error) { |
||||||
|
output(`❌ ${cardId} failed: ${error.message}`, 'error'); |
||||||
|
results.failed++; |
||||||
|
results.errors.push({ cardId, error: error.message }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
output(''); |
||||||
|
output(`📊 Test Results:`, 'warning'); |
||||||
|
output(`✅ Passed: ${results.passed}`, 'success'); |
||||||
|
output(`❌ Failed: ${results.failed}`, 'error'); |
||||||
|
output(`📈 Total: ${results.passed + results.failed}`); |
||||||
|
|
||||||
|
updateStats(results); |
||||||
|
}; |
||||||
|
|
||||||
|
window.checkCoverage = function() { |
||||||
|
const allCardIds = Object.keys(CARDS); |
||||||
|
const testedCardIds = Object.keys(sampleTests); |
||||||
|
const coverage = ((testedCardIds.length / allCardIds.length) * 100).toFixed(1); |
||||||
|
|
||||||
|
clearOutput(); |
||||||
|
output('📋 Test Coverage Analysis:', 'warning'); |
||||||
|
output(`Total cards: ${allCardIds.length}`); |
||||||
|
output(`Sample tested cards: ${testedCardIds.length}`); |
||||||
|
output(`Sample coverage: ${coverage}%`); |
||||||
|
output(''); |
||||||
|
output('Note: This is a sample test suite. Full test suite is in card-tests.js', 'warning'); |
||||||
|
|
||||||
|
document.getElementById('coveragePercent').textContent = coverage + '%'; |
||||||
|
document.getElementById('stats').style.display = 'flex'; |
||||||
|
}; |
||||||
|
|
||||||
|
window.runSpecificTest = function() { |
||||||
|
const cardId = prompt('Enter card ID to test:'); |
||||||
|
if (!cardId) return; |
||||||
|
|
||||||
|
if (!CARDS[cardId]) { |
||||||
|
output(`❌ Card '${cardId}' not found`, 'error'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
clearOutput(); |
||||||
|
output(`Testing specific card: ${cardId}`, 'warning'); |
||||||
|
|
||||||
|
try { |
||||||
|
const result = testCard(cardId); |
||||||
|
output(`✅ ${cardId} executed successfully`, 'success'); |
||||||
|
output(`Initial state: ${JSON.stringify(result.initialState, null, 2)}`); |
||||||
|
output(`Final state: ${JSON.stringify(result.finalState, null, 2)}`); |
||||||
|
output(`Logs: ${result.logs.join(', ')}`); |
||||||
|
if (Object.keys(result.flags).length > 0) { |
||||||
|
output(`Flags: ${JSON.stringify(result.flags, null, 2)}`); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
output(`❌ ${cardId} failed: ${error.message}`, 'error'); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Make functions global |
||||||
|
window.clearOutput = clearOutput; |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
||||||
Loading…
Reference in new issue