1c7fc8c551
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m18s
- Add fabricator-recipes.ts with 12 recipes across earth/metal/crystal/sand mana types - Add FabricatorSubTab with mana-type filtering, recipe cards, materials inventory - Add EnchanterSubTab integrating existing 3-phase flow (Design → Prepare → Apply) - Add CraftingTab main component with clsx-based sub-tab system (matches DisciplinesTab pattern) - Wire into tabs barrel export and page.tsx with lazy loading + DebugName wrapper - Add 17 tests covering exports, displayNames, recipe data integrity, helpers, file sizes - All files under 400 lines
181 lines
7.4 KiB
TypeScript
181 lines
7.4 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
|
|
// ─── Test: CraftingTab barrel export ──────────────────────────────────────────
|
|
|
|
describe('CraftingTab module structure', () => {
|
|
it('exports CraftingTab from its module', async () => {
|
|
const mod = await import('./CraftingTab');
|
|
expect(mod.CraftingTab).toBeDefined();
|
|
expect(typeof mod.CraftingTab).toBe('function');
|
|
});
|
|
|
|
it('CraftingTab has correct displayName', async () => {
|
|
const { CraftingTab } = await import('./CraftingTab');
|
|
expect(CraftingTab.displayName).toBe('CraftingTab');
|
|
});
|
|
});
|
|
|
|
// ─── Test: CraftingTab in tabs barrel index ────────────────────────────────────
|
|
|
|
describe('Tab barrel export', () => {
|
|
it('includes CraftingTab in the tabs index', async () => {
|
|
const mod = await import('@/components/game/tabs');
|
|
expect(mod.CraftingTab).toBeDefined();
|
|
expect(typeof mod.CraftingTab).toBe('function');
|
|
});
|
|
});
|
|
|
|
// ─── Test: FabricatorSubTab ────────────────────────────────────────────────────
|
|
|
|
describe('FabricatorSubTab module', () => {
|
|
it('exports FabricatorSubTab', async () => {
|
|
const mod = await import('./CraftingTab/FabricatorSubTab');
|
|
expect(mod.FabricatorSubTab).toBeDefined();
|
|
expect(typeof mod.FabricatorSubTab).toBe('function');
|
|
});
|
|
|
|
it('FabricatorSubTab has correct displayName', async () => {
|
|
const { FabricatorSubTab } = await import('./CraftingTab/FabricatorSubTab');
|
|
expect(FabricatorSubTab.displayName).toBe('FabricatorSubTab');
|
|
});
|
|
});
|
|
|
|
// ─── Test: EnchanterSubTab ─────────────────────────────────────────────────────
|
|
|
|
describe('EnchanterSubTab module', () => {
|
|
it('exports EnchanterSubTab', async () => {
|
|
const mod = await import('./CraftingTab/EnchanterSubTab');
|
|
expect(mod.EnchanterSubTab).toBeDefined();
|
|
expect(typeof mod.EnchanterSubTab).toBe('function');
|
|
});
|
|
|
|
it('EnchanterSubTab has correct displayName', async () => {
|
|
const { EnchanterSubTab } = await import('./CraftingTab/EnchanterSubTab');
|
|
expect(EnchanterSubTab.displayName).toBe('EnchanterSubTab');
|
|
});
|
|
});
|
|
|
|
// ─── Test: Fabricator recipes data ─────────────────────────────────────────────
|
|
|
|
describe('Fabricator recipes data', () => {
|
|
it('exports FABRICATOR_RECIPES', async () => {
|
|
const mod = await import('@/lib/game/data/fabricator-recipes');
|
|
expect(mod.FABRICATOR_RECIPES).toBeDefined();
|
|
expect(Object.keys(mod.FABRICATOR_RECIPES).length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('all recipes have required fields', async () => {
|
|
const { FABRICATOR_RECIPES } = await import('@/lib/game/data/fabricator-recipes');
|
|
for (const recipe of Object.values(FABRICATOR_RECIPES)) {
|
|
expect(recipe.id).toBeTruthy();
|
|
expect(recipe.name).toBeTruthy();
|
|
expect(recipe.description).toBeTruthy();
|
|
expect(recipe.manaType).toBeTruthy();
|
|
expect(recipe.equipmentTypeId).toBeTruthy();
|
|
expect(recipe.slot).toBeTruthy();
|
|
expect(recipe.materials).toBeDefined();
|
|
expect(recipe.manaCost).toBeGreaterThan(0);
|
|
expect(recipe.craftTime).toBeGreaterThan(0);
|
|
expect(recipe.rarity).toBeTruthy();
|
|
expect(recipe.gearTrait).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
it('only uses valid solid/structural mana types', async () => {
|
|
const { FABRICATOR_RECIPES } = await import('@/lib/game/data/fabricator-recipes');
|
|
const validManaTypes = new Set(['earth', 'metal', 'crystal', 'sand']);
|
|
for (const recipe of Object.values(FABRICATOR_RECIPES)) {
|
|
expect(validManaTypes.has(recipe.manaType)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('no fire, water, air, light, dark, death, stellar, or void gear recipes', async () => {
|
|
const { FABRICATOR_RECIPES } = await import('@/lib/game/data/fabricator-recipes');
|
|
const invalidTypes = new Set(['fire', 'water', 'air', 'light', 'dark', 'death', 'stellar', 'void']);
|
|
for (const recipe of Object.values(FABRICATOR_RECIPES)) {
|
|
expect(invalidTypes.has(recipe.manaType)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('getRecipesByManaType filters correctly', async () => {
|
|
const { getRecipesByManaType, FABRICATOR_RECIPES } = await import('@/lib/game/data/fabricator-recipes');
|
|
const earthRecipes = getRecipesByManaType('earth');
|
|
expect(earthRecipes.length).toBeGreaterThan(0);
|
|
for (const r of earthRecipes) {
|
|
expect(r.manaType).toBe('earth');
|
|
}
|
|
|
|
const empty = getRecipesByManaType('nonexistent');
|
|
expect(empty).toEqual([]);
|
|
});
|
|
|
|
it('getRecipeById returns correct recipe', async () => {
|
|
const { getRecipeById, FABRICATOR_RECIPES } = await import('@/lib/game/data/fabricator-recipes');
|
|
const firstId = Object.values(FABRICATOR_RECIPES)[0].id;
|
|
const recipe = getRecipeById(firstId);
|
|
expect(recipe).toBeDefined();
|
|
expect(recipe!.id).toBe(firstId);
|
|
|
|
const missing = getRecipeById('nonexistent');
|
|
expect(missing).toBeUndefined();
|
|
});
|
|
|
|
it('canCraftRecipe returns correct availability', async () => {
|
|
const { canCraftRecipe, FABRICATOR_RECIPES } = await import('@/lib/game/data/fabricator-recipes');
|
|
const recipe = Object.values(FABRICATOR_RECIPES)[0];
|
|
|
|
// Empty materials => cannot craft
|
|
const result1 = canCraftRecipe(recipe, {}, 0);
|
|
expect(result1.canCraft).toBe(false);
|
|
expect(Object.keys(result1.missingMaterials).length).toBeGreaterThan(0);
|
|
expect(result1.missingMana).toBeGreaterThan(0);
|
|
|
|
// Sufficient materials and mana => can craft
|
|
const sufficientMats: Record<string, number> = {};
|
|
for (const [matId, amount] of Object.entries(recipe.materials)) {
|
|
sufficientMats[matId] = amount;
|
|
}
|
|
const result2 = canCraftRecipe(recipe, sufficientMats, recipe.manaCost);
|
|
expect(result2.canCraft).toBe(true);
|
|
expect(Object.keys(result2.missingMaterials)).toHaveLength(0);
|
|
expect(result2.missingMana).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ─── Test: File size limits ────────────────────────────────────────────────────
|
|
|
|
describe('File size limits (400 lines max)', () => {
|
|
it('CraftingTab.tsx is under 400 lines', async () => {
|
|
const fs = await import('fs');
|
|
const path = await import('path');
|
|
const content = fs.readFileSync(
|
|
path.join(__dirname, 'CraftingTab.tsx'),
|
|
'utf-8',
|
|
);
|
|
const lines = content.split('\n').length;
|
|
expect(lines).toBeLessThan(400);
|
|
});
|
|
|
|
it('FabricatorSubTab.tsx is under 400 lines', async () => {
|
|
const fs = await import('fs');
|
|
const path = await import('path');
|
|
const content = fs.readFileSync(
|
|
path.join(__dirname, 'CraftingTab', 'FabricatorSubTab.tsx'),
|
|
'utf-8',
|
|
);
|
|
const lines = content.split('\n').length;
|
|
expect(lines).toBeLessThan(400);
|
|
});
|
|
|
|
it('EnchanterSubTab.tsx is under 400 lines', async () => {
|
|
const fs = await import('fs');
|
|
const path = await import('path');
|
|
const content = fs.readFileSync(
|
|
path.join(__dirname, 'CraftingTab', 'EnchanterSubTab.tsx'),
|
|
'utf-8',
|
|
);
|
|
const lines = content.split('\n').length;
|
|
expect(lines).toBeLessThan(400);
|
|
});
|
|
});
|