Files
idlegame/src/content/__tests__/schema.test.ts
T

229 lines
6.9 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { buildContent } from '../schema';
const validResources = [{ id: 'gold', name: 'Gold' }];
const validActions = [
{
id: 'forage',
name: 'Forage',
group: { id: 'camp', label: 'Camp' },
durationMs: 3000,
yields: [{ resourceId: 'gold', amount: 1 }],
},
];
describe('buildContent()', () => {
it('validates definitions and indexes them by id', () => {
const content = buildContent({ resources: validResources, actions: validActions });
expect(content.resourcesById.gold.name).toBe('Gold');
expect(content.actionsById.forage.durationMs).toBe(3000);
});
it('applies the startAmount default of 0', () => {
const content = buildContent({ resources: validResources, actions: validActions });
expect(content.resourcesById.gold.startAmount).toBe(0);
});
it('rejects an action that yields an unknown resource', () => {
const actions = [
{
id: 'forage',
name: 'Forage',
group: { id: 'camp', label: 'Camp' },
durationMs: 3000,
yields: [{ resourceId: 'ghost', amount: 1 }],
},
];
expect(() => buildContent({ resources: validResources, actions })).toThrow(/unknown resource/i);
});
it('rejects duplicate resource ids', () => {
const resources = [
{ id: 'gold', name: 'Gold' },
{ id: 'gold', name: 'Gold Again' },
];
expect(() => buildContent({ resources, actions: validActions })).toThrow(/duplicate/i);
});
it('rejects a structurally invalid definition', () => {
const actions = [
{
id: 'forage',
name: 'Forage',
group: { id: 'camp', label: 'Camp' },
durationMs: -1,
yields: [{ resourceId: 'gold', amount: 1 }],
},
];
expect(() => buildContent({ resources: validResources, actions })).toThrow();
});
});
describe('costs and multi-yield', () => {
it('accepts optional costs and multiple yields', () => {
const resources = [
{ id: 'gold', name: 'Gold' },
{ id: 'wood', name: 'Wood' },
];
const actions = [
{
id: 'craft',
name: 'Craft',
group: { id: 'camp', label: 'Camp' },
durationMs: 1000,
costs: [{ resourceId: 'wood', amount: 2 }],
yields: [
{ resourceId: 'gold', amount: 1 },
{ resourceId: 'wood', amount: 1 },
],
},
];
const content = buildContent({ resources, actions });
expect(content.actionsById.craft.costs).toEqual([{ resourceId: 'wood', amount: 2 }]);
expect(content.actionsById.craft.yields).toHaveLength(2);
});
it('rejects cost referencing unknown resource', () => {
const actions = [
{
id: 'craft',
name: 'Craft',
group: { id: 'camp', label: 'Camp' },
durationMs: 1000,
costs: [{ resourceId: 'ghost', amount: 1 }],
yields: [{ resourceId: 'gold', amount: 1 }],
},
];
expect(() => buildContent({ resources: validResources, actions })).toThrow(/unknown resource/i);
});
it('defaults costs to empty array', () => {
const content = buildContent({ resources: validResources, actions: validActions });
expect(content.actionsById.forage.costs).toEqual([]);
});
});
describe('unlock conditions', () => {
it('accepts optional minResources and requireStoryFlags', () => {
const actions = [
{
id: 'forage',
name: 'Forage',
group: { id: 'camp', label: 'Camp' },
durationMs: 3000,
yields: [{ resourceId: 'gold', amount: 1 }],
unlock: {
minResources: { gold: 10 },
requireStoryFlags: ['intro_complete'],
},
},
];
const content = buildContent({ resources: validResources, actions });
expect(content.actionsById.forage.unlock).toEqual({
minResources: { gold: 10 },
requireStoryFlags: ['intro_complete'],
});
});
it('defaults unlock to undefined when omitted', () => {
const content = buildContent({ resources: validResources, actions: validActions });
expect(content.actionsById.forage.unlock).toBeUndefined();
});
});
describe('action narrative fields', () => {
it('accepts optional storyHint and storyTooltip', () => {
const actions = [
{
id: 'forage',
name: 'Forage',
group: { id: 'camp', label: 'Camp' },
durationMs: 3000,
yields: [{ resourceId: 'gold', amount: 1 }],
storyHint: 'Gather what the forest offers.',
storyTooltip: 'Your first step into the wild.',
},
];
const content = buildContent({ resources: validResources, actions });
expect(content.actionsById.forage.storyHint).toBe('Gather what the forest offers.');
expect(content.actionsById.forage.storyTooltip).toBe('Your first step into the wild.');
});
it('defaults storyHint and storyTooltip to undefined when omitted', () => {
const content = buildContent({ resources: validResources, actions: validActions });
expect(content.actionsById.forage.storyHint).toBeUndefined();
expect(content.actionsById.forage.storyTooltip).toBeUndefined();
});
});
describe('action kind schema', () => {
it('accepts kind, group, and storyChoiceId', () => {
const content = buildContent({
resources: [{ id: 'supplies', name: 'Supplies' }],
actions: [
{
id: 'pick_high_road',
name: 'Take the high road',
kind: 'story',
group: { id: 'fork', label: 'Crossroads' },
storyChoiceId: 'pick_a',
yields: [],
},
],
});
expect(content.actionsById.pick_high_road.kind).toBe('story');
expect(content.actionsById.pick_high_road.group.label).toBe('Crossroads');
});
it('defaults kind to timed and requires durationMs for timed actions', () => {
expect(() =>
buildContent({
resources: [{ id: 'supplies', name: 'Supplies' }],
actions: [
{
id: 'broken',
name: 'Broken',
kind: 'timed',
group: { id: 'camp', label: 'Camp' },
yields: [{ resourceId: 'supplies', amount: 1 }],
},
],
}),
).toThrow();
});
it('requires durationMs for loop actions', () => {
const content = buildContent({
resources: [{ id: 'supplies', name: 'Supplies' }],
actions: [
{
id: 'rest',
name: 'Rest',
kind: 'loop',
group: { id: 'camp_loop', label: 'Camp activities' },
durationMs: 2000,
yields: [{ resourceId: 'supplies', amount: 1 }],
},
],
});
expect(content.actionsById.rest.kind).toBe('loop');
});
it('rejects loop action without durationMs', () => {
expect(() =>
buildContent({
resources: [{ id: 'supplies', name: 'Supplies' }],
actions: [
{
id: 'broken_loop',
name: 'Broken loop',
kind: 'loop',
group: { id: 'camp', label: 'Camp' },
yields: [],
},
],
}),
).toThrow(/durationMs/);
});
});